package recognize

import (
	"log/slog"
	"strings"
	"unicode"

	"git.vakhrushev.me/av/jellybit/internal/metadata"
)

// homoglyphPairs — курируемая таблица визуально неотличимых кирилло-латинских
// пар (двойников). Реальная боль — русскоклавиатурные двойники в англоязычных
// названиях (кейс «Hаrold» с кир. `а`). Таблица используется в обе стороны:
// направление свёртки выбирает foldHomoglyphs по доминирующему скрипту токена.
// Единственный источник правды и для санитайзинга плана, и для нормализации в
// гейте матча (normalize).
var homoglyphPairs = []struct{ cyr, lat rune }{
	// строчные
	{'а', 'a'}, {'е', 'e'}, {'о', 'o'}, {'р', 'p'}, {'с', 'c'},
	{'у', 'y'}, {'х', 'x'}, {'к', 'k'},
	// заглавные
	{'А', 'A'}, {'В', 'B'}, {'Е', 'E'}, {'К', 'K'}, {'М', 'M'},
	{'Н', 'H'}, {'О', 'O'}, {'Р', 'P'}, {'С', 'C'}, {'Т', 'T'},
	{'Х', 'X'},
}

var (
	cyr2lat = map[rune]rune{}
	lat2cyr = map[rune]rune{}
)

func init() {
	for _, p := range homoglyphPairs {
		cyr2lat[p.cyr] = p.lat
		lat2cyr[p.lat] = p.cyr
	}
}

// SanitizeTitle чистит человекочитаемое поле плана как недоверенный вывод LLM:
// (1) убирает управляющие и zero-width символы; (2) сводит пробелы к одиночным и
// обрезает края; (3) сворачивает homoglyph-двойники. Порядок важен: strip делаем
// до collapse, чтобы удаление zero-width не оставляло сдвоенных пробелов.
func SanitizeTitle(s string) string {
	s = stripControl(s)
	s = collapseSpaces(s)
	s = foldHomoglyphs(s)
	return s
}

// UsableTitle отвечает, годится ли название как компонент пути к файлу. Годится
// то, в чём после санитайзинга осталась хотя бы одна буква или цифра: layout
// снимает разделители и обрезает края от точек и пробелов, поэтому «.», «...»,
// «-» и прочая голая пунктуация схлопываются там в пустое имя каталога, а
// ведущая точка вдобавок делает каталог скрытым для Jellyfin.
//
// Проверять этим предикатом надо УЖЕ санитизированное значение: невидимые
// символы буквами не являются, но и не мешают — их снимает SanitizeTitle.
// Экспортирована вместе с SanitizeTitle, потому что тот же вопрос задаёт worker
// при закреплении выбранного человеком источника: правило одно, домов у него
// быть не должно.
func UsableTitle(s string) bool {
	for _, r := range s {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			return true
		}
	}
	return false
}

// sanitizeCandidate чистит человекочитаемые названия кандидата метабазы. Зовётся
// на КОПИИ кандидата в момент, когда он уходит из сверки дальше — в список для
// review, в хранилище, на экран и в закрепляемое человеком значение. Значения,
// по которым ищется сильный матч, при этом не меняются: сравнение обязано идти
// по тому, что отдал провайдер (см. metadata-match), иначе кандидат, отличающийся
// от плана невидимым символом, начал бы совпадать там, где прежде уходил в review.
func sanitizeCandidate(c metadata.Candidate) metadata.Candidate {
	c.Title = SanitizeTitle(c.Title)
	c.OriginalTitle = SanitizeTitle(c.OriginalTitle)
	return c
}

// sanitizePlan применяет санитайзинг к человекочитаемым полям плана. files[].src
// НЕ трогаем: они обязаны байт-в-байт совпадать с реальными файлами торрента, и
// homoglyph там — настоящий mismatch (отклоняется валидацией, уходит в review),
// а не повод «чинить» путь. Логируем на Debug, когда значение реально изменилось
// (названия не относятся к секретам).
func sanitizePlan(p *Plan, log *slog.Logger) {
	p.Title = sanitizeField(p.Title, "title", log)
	p.OriginalTitle = sanitizeField(p.OriginalTitle, "original_title", log)
	p.ProviderHint = sanitizeField(p.ProviderHint, "provider_hint", log)
}

func sanitizeField(v, field string, log *slog.Logger) string {
	clean := SanitizeTitle(v)
	if clean != v && log != nil {
		log.Debug("recognition plan field sanitized", "field", field, "before", v, "after", clean)
	}
	return clean
}

// stripControl удаляет format-символы (zero-width, BOM, soft-hyphen — категория
// Cf) и управляющие C0/C1, кроме пробельных (\t\n\r и пр. остаются — их сведёт
// collapseSpaces).
func stripControl(s string) string {
	return strings.Map(func(r rune) rune {
		switch {
		case unicode.Is(unicode.Cf, r):
			return -1
		case unicode.IsControl(r) && !unicode.IsSpace(r):
			return -1
		default:
			return r
		}
	}, s)
}

// collapseSpaces сводит последовательности пробельных к одиночному пробелу и
// обрезает края.
func collapseSpaces(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	pendingSpace := false
	for _, r := range s {
		if unicode.IsSpace(r) {
			pendingSpace = true
			continue
		}
		if pendingSpace && b.Len() > 0 {
			b.WriteByte(' ')
		}
		pendingSpace = false
		b.WriteRune(r)
	}
	return b.String()
}

// foldHomoglyphs сворачивает кирилло-латинские homoglyph-двойники потокенно.
// Токен (максимальная последовательность букв) из одного скрипта не трогаем —
// билингвальность реальна, честная кириллица неприкосновенна. В смешанном токене
// определяем доминирующий скрипт по числу буквенных рун и заменяем
// буквы-меньшинство их двойниками из доминирующего скрипта; при равенстве
// скриптов токен не меняем. Не-буквенные символы (пробелы, цифры, пунктуация)
// разделяют токены и копируются как есть.
func foldHomoglyphs(s string) string {
	runes := []rune(s)
	var b strings.Builder
	b.Grow(len(s))
	for i := 0; i < len(runes); {
		if !unicode.IsLetter(runes[i]) {
			b.WriteRune(runes[i])
			i++
			continue
		}
		j := i
		for j < len(runes) && unicode.IsLetter(runes[j]) {
			j++
		}
		b.WriteString(foldToken(runes[i:j]))
		i = j
	}
	return b.String()
}

func foldToken(tok []rune) string {
	var cyr, lat int
	for _, r := range tok {
		switch {
		case unicode.Is(unicode.Cyrillic, r):
			cyr++
		case unicode.Is(unicode.Latin, r):
			lat++
		}
	}
	if cyr == 0 || lat == 0 {
		return string(tok) // одно-скриптовый токен — не трогаем
	}
	var m map[rune]rune
	switch {
	case lat > cyr:
		m = cyr2lat // доминирует латиница — сворачиваем кириллические двойники
	case cyr > lat:
		m = lat2cyr
	default:
		return string(tok) // нет доминирующего скрипта — не трогаем
	}
	out := make([]rune, len(tok))
	for i, r := range tok {
		if repl, ok := m[r]; ok {
			out[i] = repl
		} else {
			out[i] = r
		}
	}
	return string(out)
}
