package torrentfile

import (
	"bytes"
	"crypto/sha1"
	"errors"
	"fmt"
	"os"
	"path"

	"github.com/jackpal/bencode-go"
)

type bencodeTorrent struct {
	Announce     string      `bencode:"announce"`
	AnnounceList [][]string  `bencode:"announce-list"`
	Info         bencodeInfo `bencode:"info"`
}

type bencodeInfo struct {
	Pieces      string `bencode:"pieces"`
	PieceLength int    `bencode:"piece length"`
	Length      int    `bencode:"length"`
	Files       []file `bencode:"files"`
	Name        string `bencode:"name"`
}

type file struct {
	Length int      `bencode:"length"`
	Path   []string `bencode:"path"`
}

type File struct {
	Path   string
	Length int
}

type TorrentFile struct {
	Announce    string
	Trackers    []string
	InfoHash    [20]byte
	PieceHashes [][20]byte
	PieceLength int
	Length      int
	Name        string
	Files       []File
}

func Open(path string) (*TorrentFile, error) {
	rawData, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	infoBytes, err := extractInfoBytes(rawData)
	if err != nil {
		return nil, err
	}

	var bto bencodeTorrent

	if err := bencode.Unmarshal(bytes.NewReader(rawData), &bto); err != nil {
		return nil, err
	}

	if err := validateMetadata(bto.Info, bto.Announce); err != nil {
		return nil, err
	}

	pieceHashes, err := splitPieceHashes(bto.Info.Pieces)
	if err != nil {
		return nil, err
	}

	files, totalLength, err := deriveFilesAndLength(bto.Info)
	if err != nil {
		return nil, err
	}

	if err := validatePieceCount(totalLength, bto.Info.PieceLength, len(pieceHashes)); err != nil {
		return nil, err
	}

	infoHash := sha1.Sum(infoBytes)
	trackers := collectTrackers(bto.Announce, bto.AnnounceList)

	tf := TorrentFile{
		Announce:    bto.Announce,
		Trackers:    trackers,
		InfoHash:    infoHash,
		PieceHashes: pieceHashes,
		PieceLength: bto.Info.PieceLength,
		Length:      totalLength,
		Name:        bto.Info.Name,
		Files:       files,
	}

	return &tf, nil
}

func validateMetadata(info bencodeInfo, announce string) error {
	switch {
	case announce == "":
		return errors.New("torrent announce URL is empty")
	case info.Name == "":
		return errors.New("torrent name is empty")
	case info.PieceLength <= 0:
		return errors.New("torrent piece length must be greater than zero")
	}

	switch {
	case info.Length > 0 && len(info.Files) > 0:
		return errors.New("torrent contains both single-file and multi-file metadata")
	case info.Length <= 0 && len(info.Files) == 0:
		return errors.New("torrent missing both length and files metadata")
	}

	return nil
}

func splitPieceHashes(rawPieces string) ([][20]byte, error) {
	pieces := []byte(rawPieces)
	if len(pieces) == 0 {
		return nil, errors.New("torrent has no piece hashes")
	}

	if len(pieces)%sha1.Size != 0 {
		return nil, fmt.Errorf("invalid pieces data size %d (must be multiple of %d)", len(pieces), sha1.Size)
	}

	hashes := make([][20]byte, len(pieces)/sha1.Size)
	for i := range hashes {
		start := i * sha1.Size
		copy(hashes[i][:], pieces[start:start+sha1.Size])
	}

	return hashes, nil
}

func deriveFilesAndLength(info bencodeInfo) ([]File, int, error) {
	if info.Length > 0 {
		return []File{{Path: info.Name, Length: info.Length}}, info.Length, nil
	}

	files := make([]File, 0, len(info.Files))
	totalLength := 0
	for _, f := range info.Files {
		if f.Length <= 0 {
			return nil, 0, errors.New("torrent file length must be greater than zero")
		}
		if len(f.Path) == 0 {
			return nil, 0, errors.New("torrent file path is empty")
		}

		parts := make([]string, 0, len(f.Path)+1)
		parts = append(parts, info.Name)
		parts = append(parts, f.Path...)
		filePath := path.Join(parts...)
		if filePath == "." {
			return nil, 0, errors.New("torrent file path is invalid")
		}

		files = append(files, File{
			Path:   filePath,
			Length: f.Length,
		})
		totalLength += f.Length
	}

	if totalLength <= 0 {
		return nil, 0, errors.New("torrent total length must be greater than zero")
	}

	return files, totalLength, nil
}

func validatePieceCount(totalLength, pieceLength, actualPieces int) error {
	expectedPieces := (totalLength + pieceLength - 1) / pieceLength
	if expectedPieces != actualPieces {
		return fmt.Errorf("piece hash count mismatch: expected %d, got %d", expectedPieces, actualPieces)
	}
	return nil
}

func extractInfoBytes(data []byte) ([]byte, error) {
	if len(data) == 0 || data[0] != 'd' {
		return nil, errors.New("torrent root must be a bencoded dictionary")
	}

	idx := 1
	for idx < len(data) {
		if data[idx] == 'e' {
			break
		}

		key, next, err := parseBencodeString(data, idx)
		if err != nil {
			return nil, err
		}
		idx = next

		valueStart := idx
		valueEnd, err := skipBencodeValue(data, idx)
		if err != nil {
			return nil, err
		}

		if string(key) == "info" {
			return data[valueStart:valueEnd], nil
		}
		idx = valueEnd
	}

	return nil, errors.New("torrent does not contain top-level info dictionary")
}

func skipBencodeValue(data []byte, idx int) (int, error) {
	if idx >= len(data) {
		return 0, errors.New("unexpected end of bencoded data")
	}

	switch c := data[idx]; {
	case c == 'i':
		return skipBencodeInt(data, idx)
	case c == 'l':
		idx++
		for {
			if idx >= len(data) {
				return 0, errors.New("unexpected end while parsing bencoded list")
			}
			if data[idx] == 'e' {
				return idx + 1, nil
			}
			next, err := skipBencodeValue(data, idx)
			if err != nil {
				return 0, err
			}
			idx = next
		}
	case c == 'd':
		idx++
		for {
			if idx >= len(data) {
				return 0, errors.New("unexpected end while parsing bencoded dictionary")
			}
			if data[idx] == 'e' {
				return idx + 1, nil
			}
			_, next, err := parseBencodeString(data, idx)
			if err != nil {
				return 0, err
			}
			idx = next

			next, err = skipBencodeValue(data, idx)
			if err != nil {
				return 0, err
			}
			idx = next
		}
	case c >= '0' && c <= '9':
		_, next, err := parseBencodeString(data, idx)
		return next, err
	default:
		return 0, fmt.Errorf("invalid bencode token %q at index %d", c, idx)
	}
}

func skipBencodeInt(data []byte, idx int) (int, error) {
	if data[idx] != 'i' {
		return 0, fmt.Errorf("expected integer token at index %d", idx)
	}
	idx++
	if idx >= len(data) {
		return 0, errors.New("unexpected end while parsing bencoded integer")
	}

	if data[idx] == '-' {
		idx++
		if idx >= len(data) {
			return 0, errors.New("unexpected end after bencoded integer sign")
		}
	}

	if data[idx] < '0' || data[idx] > '9' {
		return 0, fmt.Errorf("invalid bencoded integer digit at index %d", idx)
	}

	for idx < len(data) && data[idx] != 'e' {
		if data[idx] < '0' || data[idx] > '9' {
			return 0, fmt.Errorf("invalid bencoded integer digit at index %d", idx)
		}
		idx++
	}

	if idx >= len(data) || data[idx] != 'e' {
		return 0, errors.New("unterminated bencoded integer")
	}

	return idx + 1, nil
}

func parseBencodeString(data []byte, idx int) ([]byte, int, error) {
	length, valueStart, err := parseBencodeStringLength(data, idx)
	if err != nil {
		return nil, 0, err
	}

	valueEnd := valueStart + length
	if valueEnd > len(data) {
		return nil, 0, errors.New("bencoded string length exceeds input size")
	}

	return data[valueStart:valueEnd], valueEnd, nil
}

func parseBencodeStringLength(data []byte, idx int) (int, int, error) {
	if idx >= len(data) {
		return 0, 0, errors.New("unexpected end while parsing bencoded string length")
	}
	if data[idx] < '0' || data[idx] > '9' {
		return 0, 0, fmt.Errorf("invalid bencoded string length token at index %d", idx)
	}

	length := 0
	for idx < len(data) && data[idx] >= '0' && data[idx] <= '9' {
		length = (length * 10) + int(data[idx]-'0')
		idx++
	}

	if idx >= len(data) || data[idx] != ':' {
		return 0, 0, errors.New("unterminated bencoded string length")
	}

	return length, idx + 1, nil
}

func collectTrackers(primary string, announceList [][]string) []string {
	seen := make(map[string]struct{})
	trackers := make([]string, 0, 1+len(announceList))

	add := func(tracker string) {
		if tracker == "" {
			return
		}
		if _, ok := seen[tracker]; ok {
			return
		}
		seen[tracker] = struct{}{}
		trackers = append(trackers, tracker)
	}

	add(primary)
	for _, tier := range announceList {
		for _, tracker := range tier {
			add(tracker)
		}
	}

	return trackers
}
