package service

import (
	"context"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"time"

	"github.com/TopherMayor/unified-media-manager/internal/db"
)

type MatchResult struct {
	MediaID    int64  `json:"media_id"`
	MediaType  string `json:"media_type"`
	Title      string `json:"title"`
	Year       *int   `json:"year,omitempty"`
	Season     *int   `json:"season,omitempty"`
	Episode    *int   `json:"episode,omitempty"`
	RootFolder string `json:"root_folder"`
	Confidence string `json:"confidence"`
}

type MatcherService struct {
	db *db.DB
}

func NewMatcherService(database *db.DB) *MatcherService {
	return &MatcherService{db: database}
}

var (
	seasonEpisodeRe = regexp.MustCompile(`(?i)[sS](\d{1,2})[eE](\d{1,2})`)
	altSeasonEpsRe  = regexp.MustCompile(`(\d{1,2})[xX](\d{1,2})`)
	bracketRe2      = regexp.MustCompile(`\[.*?\]`)
	qualityTrailRe  = regexp.MustCompile(`(?i)(?:[sS]\d{1,2}[eE]\d{1,2}|\d{3,4}[pi]|720|1080|2160|HDTV|WEB|BluRay|BRRip|BDRip|DVDRip|REMUX|x264|x265|HEVC|AAC|DTS|AC3|DD|FLAC).*$`)
	sepRe           = regexp.MustCompile(`[._-]+`)
	punctRe         = regexp.MustCompile(`[^\w\s]`)
	multiSpaceRe    = regexp.MustCompile(`\s+`)
)

func normalizeTitle(s string) string {
	s = strings.ToLower(s)
	s = punctRe.ReplaceAllString(s, " ")
	s = multiSpaceRe.ReplaceAllString(s, " ")
	return strings.TrimSpace(s)
}

func parseSeasonEpisode(s string) (season, episode int, found bool) {
	if m := seasonEpisodeRe.FindStringSubmatch(s); m != nil {
		season = atoi(m[1])
		episode = atoi(m[2])
		return season, episode, true
	}
	if m := altSeasonEpsRe.FindStringSubmatch(s); m != nil {
		season = atoi(m[1])
		episode = atoi(m[2])
		return season, episode, true
	}
	return 0, 0, false
}

func atoi(s string) int {
	n := 0
	for _, c := range s {
		if c >= '0' && c <= '9' {
			n = n*10 + int(c-'0')
		}
	}
	return n
}

func extractCleanTitle(releaseName string) string {
	cleaned := bracketRe2.ReplaceAllString(releaseName, " ")
	if m := seasonEpisodeRe.FindStringIndex(cleaned); m != nil {
		cleaned = cleaned[:m[0]]
	} else if m := qualityTrailRe.FindStringIndex(cleaned); m != nil {
		cleaned = cleaned[:m[0]]
	}
	cleaned = sepRe.ReplaceAllString(cleaned, " ")
	return normalizeTitle(cleaned)
}

func levenshteinDistance(a, b string) int {
	la, lb := len(a), len(b)
	if la == 0 {
		return lb
	}
	if lb == 0 {
		return la
	}
	prev := make([]int, lb+1)
	curr := make([]int, lb+1)
	for j := 0; j <= lb; j++ {
		prev[j] = j
	}
	for i := 1; i <= la; i++ {
		curr[0] = i
		for j := 1; j <= lb; j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			curr[j] = minOf3(
				prev[j]+1,
				curr[j-1]+1,
				prev[j-1]+cost,
			)
		}
		prev, curr = curr, prev
	}
	return prev[lb]
}

func minOf3(a, b, c int) int {
	if a < b {
		if a < c {
			return a
		}
		return c
	}
	if b < c {
		return b
	}
	return c
}

func (s *MatcherService) Match(ctx context.Context, releaseName string, mediaType string) (*MatchResult, error) {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	season, episode, hasSE := parseSeasonEpisode(releaseName)
	cleanTitle := extractCleanTitle(releaseName)

	if cleanTitle == "" {
		return &MatchResult{Confidence: "none"}, nil
	}

	qb := NewQueryBuilder(1)
	qb.AddLiteral("deleted_at IS NULL")
	qb.AddLiteral("monitored = true")

	if mediaType == "series" || hasSE {
		qb.AddLiteral("media_type IN ('series', 'episode')")
	} else if mediaType != "" {
		qb.AddLiteral("media_type NOT IN ('series', 'episode')")
		qb.Add("media_type = $%d", mediaType)
	}

	query := fmt.Sprintf("SELECT %s FROM media%s", mediaColumns, qb.Where())
	rows, err := s.db.Pool.Query(ctx, query, qb.Args()...)
	if err != nil {
		slog.Error("failed to query media for matching", "error", err)
		return nil, fmt.Errorf("query media candidates: %w", err)
	}
	defer rows.Close()

	candidates, err := scanMediaRows(rows)
	if err != nil {
		return nil, fmt.Errorf("scan media candidates: %w", err)
	}

	var exactMatch *Media
	var fuzzyMatch *Media
	var fuzzyDist int

	for i := range candidates {
		c := &candidates[i]
		norm := normalizeTitle(c.Title)

		if norm == cleanTitle {
			exactMatch = c
			break
		}

		dist := levenshteinDistance(cleanTitle, norm)
		if dist <= 2 {
			if fuzzyMatch == nil || dist < fuzzyDist {
				fuzzyMatch = c
				fuzzyDist = dist
			}
		}
	}

	matched := exactMatch
	confidence := "exact"
	if matched == nil && fuzzyMatch != nil {
		matched = fuzzyMatch
		confidence = "fuzzy"
	}

	if matched == nil {
		return &MatchResult{Confidence: "none"}, nil
	}

	result := &MatchResult{
		MediaID:    matched.ID,
		MediaType:  matched.MediaType,
		Title:      matched.Title,
		Year:       matched.Year,
		Confidence: confidence,
	}

	if hasSE {
		result.Season = &season
		result.Episode = &episode
	}

	if matched.RootFolderID != nil {
		var path string
		if err := s.db.Pool.QueryRow(ctx,
			"SELECT path FROM root_folders WHERE id = $1", *matched.RootFolderID).Scan(&path); err != nil {
			slog.Error("failed to query root folder", "error", err, "root_folder_id", *matched.RootFolderID)
		} else {
			result.RootFolder = path
		}
	}

	return result, nil
}
