package storage

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"git.insit.tech/psa/rtsp_reader-writer/writer/internal/config"
)

// Constants for detection video and audio types.
const (
	PacketTypeAV1   = 1
	PacketTypeH264  = 2
	PacketTypeH265  = 3
	PacketTypeMJPEG = 4
	PacketTypeVP8   = 5
	PacketTypeVP9   = 6

	PacketTypeG711 = 21
	PacketTypeAAC  = 22
	PacketTypeLPCM = 23
	PacketTypeOPUS = 24
)

// InterleavedPacket is the structure of each inner packet.
type InterleavedPacket struct {
	Type byte
	Pts  int64

	H264AUs     [][]byte
	LPCMSamples []byte
}

// Segment is overall structure according to each period.
type Segment struct {
	Date     string
	Duration string
	Packet   InterleavedPacket
	Packets  []InterleavedPacket
}

// writeString записывает строку: сначала int32 длина, затем данные.
func writeString(w io.Writer, s string) error {
	if err := binary.Write(w, binary.LittleEndian, int32(len(s))); err != nil {
		return err
	}
	_, err := w.Write([]byte(s))
	return err
}

// WritePacket записывает один interleaved пакет.
func WritePacket(w io.Writer, pkt InterleavedPacket) error {
	// Записываем тип пакета (1 байт)
	if err := binary.Write(w, binary.LittleEndian, pkt.Type); err != nil {
		return err
	}
	// Записываем pts (int64)
	if err := binary.Write(w, binary.LittleEndian, pkt.Pts); err != nil {
		return err
	}

	if pkt.Type == PacketTypeH264 {
		// Для H264 – AU как [][]byte.
		numAUs := int32(len(pkt.H264AUs))
		if err := binary.Write(w, binary.LittleEndian, numAUs); err != nil {
			return err
		}
		for _, au := range pkt.H264AUs {
			if err := binary.Write(w, binary.LittleEndian, int32(len(au))); err != nil {
				return err
			}
			if _, err := w.Write(au); err != nil {
				return err
			}
		}
	} else if pkt.Type == PacketTypeLPCM {
		// Для LPCM – просто длина и данные.
		if err := binary.Write(w, binary.LittleEndian, int32(len(pkt.LPCMSamples))); err != nil {
			return err
		}
		if _, err := w.Write(pkt.LPCMSamples); err != nil {
			return err
		}
	} else {
		return fmt.Errorf("неизвестный тип пакета: %d", pkt.Type)
	}
	return nil
}

// WriteHeader записывает заголовок сегмента (Start и Duration).
func WriteHeader(w io.Writer, seg Segment) error {
	var buf bytes.Buffer
	if err := writeString(&buf, seg.Date); err != nil {
		return err
	}
	if err := writeString(&buf, seg.Duration); err != nil {
		return err
	}
	segData := buf.Bytes()
	if err := binary.Write(w, binary.LittleEndian, int32(len(segData))); err != nil {
		return err
	}
	_, err := w.Write(segData)
	return err
}

// WriteInterleavedPacket записывает сегмент с пакетами.
// Теперь количество пакетов определяется динамически.
func WriteInterleavedPacket(w io.Writer, seg Segment) error {
	var buf bytes.Buffer

	if err := binary.Write(&buf, binary.LittleEndian, int32(1)); err != nil {
		return err
	}
	// Записываем каждый пакет.
	if err := WritePacket(&buf, seg.Packet); err != nil {
		return err
	}

	segData := buf.Bytes()
	if err := binary.Write(w, binary.LittleEndian, int32(len(segData))); err != nil {
		return err
	}
	_, err := w.Write(segData)
	return err
}

// partitionTime convert numbers from type STRING to type INT.
func partitionTime(time string) (startHour int, startMinute int, err error) {
	// Part hours and minutes.
	s := []byte(time)
	h := []byte{s[0], s[1]}
	m := []byte{s[3], s[4]}

	// Transforms hours and minutes into INTs.
	startHour, err = strconv.Atoi(string(h))
	if err != nil {
		return 0, 0, err
	}
	startMinute, err = strconv.Atoi(string(m))
	if err != nil {
		return 0, 0, err
	}

	return startHour, startMinute, nil
}

// CreateTXT collects filenames into TXT file.
func CreateTXT(path, date, startTime, endTime string) (string, error) {
	//fileNames := make([]string, 0)

	fileNameTXT := startTime + "_" + endTime + ".txt"
	f, err := os.Create(path + fileNameTXT)
	if err != nil {
		return "", fmt.Errorf("create file error: %s", err.Error())
	}
	defer f.Close()

	// Read the directory.
	dirEntry, err := os.ReadDir(path)
	if err != nil {
		return "", fmt.Errorf("read directory error: %s", err.Error())
	}

	// Calculate start time and end time of the required fragment.
	startHour, startMinute, endHour, endMinute := calcNeededTime(startTime, endTime)

	for i := startHour; i <= endHour; i++ {
		for j := startMinute; j < endMinute; j++ { // exclude the last minute as endMinute is the final time
			for _, entry := range dirEntry {
				if strings.Contains(entry.Name(), strconv.Itoa(i)+"-"+strconv.Itoa(j)+"-00"+"_"+date) {
					_, err = f.WriteString("file '" + entry.Name() + "'\n")
					if err != nil {
						return "", fmt.Errorf("write file error: %s", err.Error())
					}
				}
			}
		}
	}

	return fileNameTXT, nil
}

// MergeFiles merges the collected files into one MP4 file.
func MergeFiles(path, fileNamesTXT string) (string, error) {
	fileNameRes := time.Now().Format("15-04-05") + "_video.mp4"

	cmd := exec.Command("ffmpeg",
		"-f", "concat",
		"-safe", "0",
		"-fflags", "+genpts",
		"-i", path+fileNamesTXT,
		"-c", "copy",
		path+fileNameRes)

	err := cmd.Run()
	if err != nil {
		return "", fmt.Errorf("merge files error: %s", err.Error())
	}

	return fileNameRes, nil
}

/////////////////////////////////////////////////////////////////////////////////////

//// Функция copyFile находит файл в репозитории и создает его копию.
//func copyFiles(path string, filename []string) (filenameCopied []string, err error) {
//	for i := 0; i < len(filename); i++ {
//		input, err := os.Open(path + filename[i])
//		if err != nil {
//			log.Println("Ошибка открытия input файла: ", err)
//		}
//		defer func() {
//			err = input.Close()
//			if err != nil {
//				log.Println("Ошибка закрытия input файла.")
//			}
//		}()
//
//		output, err := os.Create(time.Now().Format("15-04-05") + "video.mkv")
//		if err != nil {
//			log.Println("Ошибка создания output файла: ", err)
//		}
//		defer func() {
//			err = output.Close()
//			if err != nil {
//				log.Println("Ошибка закрытия output файла.")
//			}
//
//		}()
//
//		_, err = io.Copy(output, input)
//		if err != nil {
//			log.Println("Ошибка копирования файла")
//		}
//	}
//
//	log.Println("Файлы скопированы.")
//	return filename, err
//}
//
//// MergeMKV принимает названия видеофайлов для объединения.
//func MergeMKV(filenames ...string) {
//	// Создание файла со списком видеофайлов для объединения
//	f, err := os.Create("videoList.txt")
//	if err != nil {
//		log.Fatalln("Ошибка создания файла со списком видеофайлов для объединения", err)
//	}
//	defer func() {
//		err = f.Close()
//		if err != nil {
//			log.Fatalln("Ошибка закрытия файла: ", err.Error())
//		}
//	}()
//
//	// Запись видеофайлов для объединения
//	n := len(filenames)
//
//	for i := 0; i < n; i++ {
//		_, err = f.WriteString("file '" + filenames[i] + "'\n")
//		if err != nil {
//			log.Fatalln(err)
//		}
//	}
//
//	err = mergeFfmpeg(f)
//	if err != nil {
//		log.Fatalln("Ошибка объединения видеофайлов с помощью ffmpeg: ", err)
//	}
//
//	err = os.Remove("videoList.txt")
//	if err != nil {
//		log.Println("Ошибка удаления временного файла videoList.txt: ", err)
//	}
//	log.Println("Временный файл videoList.txt успешно удален")
//}
//
//// DeleteTrimmedFragments удаляет временно созданные файлы.
//func DeleteTrimmedFragments(filenames ...string) {
//	n := len(filenames)
//
//	for i := 0; i < n; i++ {
//		err := os.Remove(filenames[i])
//		if err != nil {
//			log.Printf("Ошибка удаления временного файла %s: %s", filenames[i], err.Error())
//		}
//		log.Printf("Временный файл %s успешно удален", filenames[i])
//	}
//}
//
//
//// createFilename forms first part of the filename which contains time and date.
//func createFilename(time, date string) (fileName string) {
//	s := []byte(time)
//	s[3], s[4] = '0', '0'
//	fileName = string(s) + "_" + date
//	return fileName
//}
//
//// Добавление одного часа к строке в формате ЧЧ-ММ
//func addHour(startTime string) (fileName string) {
//	s := []byte(startTime)
//	if s[0] == '0' && s[1] == '9' {
//		s[0] = '1'
//		s[1] = '0'
//	} else if s[0] == '1' && s[1] == '9' {
//		s[0] = '2'
//		s[1] = '0'
//	} else {
//		s[1]++
//	}
//	fileName = string(s)
//	return fileName
//}
//
//// Проверка наличия файла
//func checkFile(path, fileName string) (string, error) {
//	found := false
//	filenameIn := fileName
//
//	dirEntry, err := os.ReadDir(path)
//	if err != nil {
//		return "", err
//	}
//	for _, entry := range dirEntry {
//		if strings.Contains(entry.Name(), filenameIn) {
//			filenameIn = entry.Name()
//			break
//		}
//		return "", fmt.Errorf("file %s not found", filenameIn)
//	}
//
//	err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
//		if err != nil {
//			return err // Возвращаем ошибку, если возникла
//		}
//		if !info.IsDir() && info.Name() == filenameIn {
//			log.Println("Файл обнаружен:", filePath)
//			found = true
//			return filepath.SkipDir // Останавливаем обход после нахождения
//		}
//		return nil
//	})
//
//	if err != nil {
//		return "", err
//	}
//
//	if !found {
//		return "", errors.New("file not found")
//	}
//
//	return filenameIn, nil
//}

// GetResolutions parses all resolutions of a camera.
func GetResolutions(file string) ([]string, error) {
	res, err := ReadDir(fmt.Sprintf("%s/%s", config.DirData, file))
	if err != nil {
		return nil, err
	}

	resolutions := make([]string, 0)
	for _, r := range res {
		if r.IsDir() && r.Name() != "log" {
			resolutions = append(resolutions, r.Name())
		}
	}

	return resolutions, nil
}

// UnixToTime transfers Unix time to the formatted time.
func UnixToTime(unixTimeInt int) (string, error) {
	t := time.Unix(int64(unixTimeInt), 0)

	return t.Format("15-04-05_02-01-2006"), nil
}

// FileBytes returns file size in bytes.
func FileBytes(id, res, file string) (int64, error) {
	filePath := fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file)

	info, err := os.Stat(filePath)
	if err != nil {
		return 0, err
	}

	size := info.Size()
	return size, nil
}

// GetDurAndTracks reads .insit file and returns duration and tracks of the file.
func GetDurAndTracks(id, res, file string) (int, map[string]string, error) {
	// Open file for reading.
	f, err := os.Open(fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file))
	if err != nil {
		return 0, nil, errors.New("opening file error for file: " + err.Error())
	}
	defer f.Close()

	// Read StreamID.
	var streamIDLen int32
	if err := binary.Read(f, binary.LittleEndian, &streamIDLen); err != nil {
		return 0, nil, errors.New("reading StreamID length error: " + err.Error())
	}
	streamIDBytes := make([]byte, streamIDLen)
	if _, err := io.ReadFull(f, streamIDBytes); err != nil {
		return 0, nil, errors.New("reading StreamID error: " + err.Error())
	}

	// Read header of the file.
	var segLen int32
	if err := binary.Read(f, binary.LittleEndian, &segLen); err != nil {
		return 0, nil, errors.New("reading header length error: " + err.Error())
	}
	segData := make([]byte, segLen)
	if _, err := io.ReadFull(f, segData); err != nil {
		return 0, nil, errors.New("reading header error: " + err.Error())
	}
	headerReader := bytes.NewReader(segData)
	headerSeg, err := ReadHeaderSegment(headerReader)
	if err != nil {
		return 0, nil, errors.New("reading header error: " + err.Error())
	}

	// Parse duration of a segment.
	per, err := strconv.Atoi(headerSeg.Duration)
	if err != nil {
		return 0, nil, errors.New("parsing duration error: " + err.Error())
	}

	tracks := make(map[string]string, 2)

	for {
		// Read segments length.
		var segLen int32
		err := binary.Read(f, binary.LittleEndian, &segLen)
		if err != nil {
			if err == io.EOF {
				break
			}
			return 0, nil, errors.New("read segments length error: " + err.Error())
		}
		// Read segments data.
		segData = make([]byte, segLen)
		if _, err := io.ReadFull(f, segData); err != nil {
			return 0, nil, errors.New("read segments error: " + err.Error())
		}
		packetReader := bytes.NewReader(segData)
		packets, err := ReadPacketSegment(packetReader)
		if err != nil {
			return 0, nil, errors.New("func readPacketSegment error: " + err.Error())
		}

		for _, pkt := range packets {
			switch pkt.Type {
			case PacketTypeH264:
				tracks["video"] = "H264"
			case PacketTypeLPCM:
				tracks["audio"] = "LPCM"
			}
		}
	}
	return per, tracks, nil
}

// readString reads string length and then reads string data.
func readString(r io.Reader) (string, error) {
	var length int32
	if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
		return "", err
	}
	buf := make([]byte, length)
	if _, err := io.ReadFull(r, buf); err != nil {
		return "", err
	}
	return string(buf), nil
}

// ReadHeaderSegment reads header of the segment.
func ReadHeaderSegment(r io.Reader) (Segment, error) {
	var seg Segment
	date, err := readString(r)
	if err != nil {
		return seg, err
	}
	seg.Date = date

	duration, err := readString(r)
	if err != nil {
		return seg, err
	}
	seg.Duration = duration

	return seg, nil
}

// readPacket reads one interleaved packet.
func readPacket(r io.Reader) (InterleavedPacket, error) {
	var pkt InterleavedPacket

	// Read type of the packet.
	typeByte := make([]byte, 1)
	if _, err := io.ReadFull(r, typeByte); err != nil {
		return pkt, err
	}
	pkt.Type = typeByte[0]

	// Read PTS (int64).
	if err := binary.Read(r, binary.LittleEndian, &pkt.Pts); err != nil {
		return pkt, err
	}

	// Read data of the segment.
	if pkt.Type == PacketTypeH264 {
		var numAUs int32
		if err := binary.Read(r, binary.LittleEndian, &numAUs); err != nil {
			return pkt, err
		}
		var auList [][]byte
		for i := 0; i < int(numAUs); i++ {
			var auLen int32
			if err := binary.Read(r, binary.LittleEndian, &auLen); err != nil {
				return pkt, err
			}
			auData := make([]byte, auLen)
			if _, err := io.ReadFull(r, auData); err != nil {
				return pkt, err
			}
			auList = append(auList, auData)
		}
		pkt.H264AUs = auList
	} else if pkt.Type == PacketTypeLPCM {
		var auLen int32
		if err := binary.Read(r, binary.LittleEndian, &auLen); err != nil {
			return pkt, err
		}
		auData := make([]byte, auLen)
		if _, err := io.ReadFull(r, auData); err != nil {
			return pkt, err
		}
		pkt.LPCMSamples = auData
	} else {
		return pkt, fmt.Errorf("unknown type of the packet: %d", pkt.Type)
	}

	return pkt, nil
}

// ReadPacketSegment reads segment packets.
func ReadPacketSegment(r io.Reader) ([]InterleavedPacket, error) {
	var numPackets int32
	if err := binary.Read(r, binary.LittleEndian, &numPackets); err != nil {
		return nil, err
	}

	var packets []InterleavedPacket
	for i := 0; i < int(numPackets); i++ {
		pkt, err := readPacket(r)
		if err != nil {
			return nil, err
		}
		packets = append(packets, pkt)
	}
	return packets, nil
}

// calcNeededTime accepts the start and  the end of the recording time, converts the time from the format STRING to the
// format INT and returns the hour and the minute of the start recording time, the hour and the minute of the end
// recording time.
func calcNeededTime(startTime, endTime string) (startHour, startMinute, endHour, endMinute int) {
	// Calc needed time.
	startHour, startMinute, err := partitionTime(startTime)
	if err != nil {
		log.Fatal("Ошибка конвертации: ", err)
	}
	endHour, endMinute, err = partitionTime(endTime)
	if err != nil {
		log.Fatal("Ошибка конвертации: ", err)
	}

	return startHour, startMinute, endHour, endMinute
}

/////////////////////////////////////////////////////////////////////////////////////

//// CalcEndMinuteFirstVideo calculates the need to change the hour (switching one fragment of video recording (which
//// lasts 1 hour) to another fragment of video recording) and returns the number of hours (required number of video
//// fragments), the duration of minutes (the object responsible for the indicator of minutes) of each fragment for the
//// formation of the final video (except for the last video fragment, provided DurationHour > 0 (the third object
//// returned by the CalcEndMinuteFirstVideo function)).
////
//// In the case that you need to take a full fragment of the video file, for example, from 00-00 to 03-00, the
//// DurationHour value is conciliated by 1 and returned.
//func CalcEndMinuteFirstVideo(durationHour, endMinuteFirstVideo, startHour, endHour, endMinute int) (
//	durationHourCalc int, endMinuteFirstVideoCalc int) {
//	durationHour = endHour - startHour
//
//	if durationHour > 0 {
//		endMinuteFirstVideo = 60
//	} else {
//		endMinuteFirstVideo = endMinute
//	}
//
//	if endMinute == 0 && durationHour > 0 {
//		durationHour -= 1
//	}
//
//	return durationHour, endMinuteFirstVideo
//}
