package tgbot

import (
	"fmt"
	"path/filepath"
	"strings"

	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"

	"git.vakhrushev.me/av/jellybit/internal/store"
	"git.vakhrushev.me/av/jellybit/internal/worker"
)

// renderCard строит текст и клавиатуру карточки по состоянию задачи.
func (b *Bot) renderCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboardMarkup) {
	id := rd.Download.ID
	state := rd.Download.State

	switch state {
	case store.StateReview, store.StateDeferred:
		return b.reviewCard(rd)
	case store.StateRecognizing:
		return "⏳ Распознаю #" + id + "…", b.webOnly(id)
	case store.StateLinking:
		return "⏳ Раскладываю #" + id + "…", nil
	case store.StateDone:
		return b.renderDone(rd), b.deletableKeyboard(id)
	default:
		text := fmt.Sprintf("Задача #%s — %s.", id, state)
		if msg := rd.Download.ErrorMsg.String; msg != "" {
			text += "\n" + msg
		}
		switch state {
		case store.StateFailed, store.StateStuck:
			// failed/stuck — даём кнопку повтора.
			return text, b.retryKeyboard(id)
		case store.StateOrphaned, store.StateTargetMissing:
			// Рассинхрон — можно подчистить остатки полным удалением.
			return text, b.deletableKeyboard(id)
		}
		return text, b.webOnly(id)
	}
}

func (b *Bot) reviewCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboardMarkup) {
	id := rd.Download.ID
	var sb strings.Builder

	fmt.Fprintf(&sb, "🟡 Нужно подтверждение #%s\n", id)
	if src := contextOrSource(rd); src != "" {
		fmt.Fprintf(&sb, "Источник: %s\n", shorten(src, 80))
	}
	fmt.Fprintf(&sb, "Похоже на: %s\n", guessLine(rd))
	if base := baseLine(rd.Recognition); base != "" {
		fmt.Fprintf(&sb, "База: %s\n", base)
	}
	if reasons := rd.Recognition.ReasonList(); len(reasons) > 0 {
		fmt.Fprintf(&sb, "Причины: %s\n", strings.Join(reasons, " · "))
	}
	if n := len(rd.Preview); n > 0 {
		fmt.Fprintf(&sb, "План: %d файлов → %s", n, tailPath(rd.Preview[0].Dst))
	}

	return strings.TrimRight(sb.String(), "\n"), b.reviewKeyboard(rd)
}

func (b *Bot) reviewKeyboard(rd *worker.ReviewData) *tgbotapi.InlineKeyboardMarkup {
	id := rd.Download.ID
	sid := id

	var row1 []tgbotapi.InlineKeyboardButton
	if len(rd.Preview) > 0 {
		row1 = append(row1, tgbotapi.NewInlineKeyboardButtonData("✅ Применить", "apply:"+sid))
	}
	row1 = append(row1, tgbotapi.NewInlineKeyboardButtonData("📺↔🎬 Тип", "type:"+sid+":"+oppositeType(string(rd.Plan.Type))))

	row2 := tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData("🔁 Уточнить", "refine:"+sid),
		tgbotapi.NewInlineKeyboardButtonData("🕗 Позже", "defer:"+sid),
	)

	var row3 []tgbotapi.InlineKeyboardButton
	if url := b.reviewURL(id); url != "" {
		row3 = append(row3, tgbotapi.NewInlineKeyboardButtonURL("🌐 В вебе", url))
	}
	row3 = append(row3, tgbotapi.NewInlineKeyboardButtonData("❌ Отклонить", "reject:"+sid))

	kb := tgbotapi.NewInlineKeyboardMarkup(row1, row2, row3)
	return &kb
}

// displayTitle — имя раздачи для уведомлений: имя раздачи (display_name, то,
// что ушло в qBittorrent и показывается в веб-UI) → распознанное название.
// Пусто, если ни того ни другого нет. Держим заголовки консистентными с UI.
func displayTitle(rd *worker.ReviewData) string {
	if t := strings.TrimSpace(rd.Download.DisplayName); t != "" {
		return t
	}
	return rd.Plan.Title
}

// renderDone — короткое сообщение о готовности.
func (b *Bot) renderDone(rd *worker.ReviewData) string {
	title := displayTitle(rd)
	if title == "" {
		title = "#" + rd.Download.ID
	}
	n := len(rd.Preview)
	if n == 0 {
		return fmt.Sprintf("✅ Готово: «%s» разложен.", title)
	}
	return fmt.Sprintf("✅ Готово: «%s» — разложено файлов: %d.", title, n)
}

// renderDesync — уведомление о рассинхроне (источник/цель удалены вручную).
func (b *Bot) renderDesync(rd *worker.ReviewData, event worker.NotifyEvent) string {
	title := displayTitle(rd)
	if title == "" {
		title = "#" + rd.Download.ID
	}
	switch event {
	case worker.EventTargetMissing:
		return fmt.Sprintf("⚠️ «%s»: файлы удалены из библиотеки, источник на месте — можно привязать заново.", title)
	case worker.EventOrphaned:
		return fmt.Sprintf("⚠️ «%s»: источник удалён из qBittorrent, библиотечная копия осталась последней (откат недоступен).", title)
	default:
		return fmt.Sprintf("⚠️ «%s»: рассинхрон состояния.", title)
	}
}

// renderFailed — уведомление об упавшей/зависшей задаче с кнопкой повтора.
func (b *Bot) renderFailed(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboardMarkup) {
	id := rd.Download.ID
	var sb strings.Builder
	verb := "не удалась"
	if rd.Download.State == store.StateStuck {
		verb = "зависла"
	}
	// Заголовок (display_name) для читаемости + #id для поиска по логам.
	if title := displayTitle(rd); title != "" {
		fmt.Fprintf(&sb, "❌ «%s» — задача #%s %s", title, id, verb)
	} else {
		fmt.Fprintf(&sb, "❌ Задача #%s %s", id, verb)
	}
	if code := rd.Download.ErrorCode.String; code != "" {
		fmt.Fprintf(&sb, " (%s)", code)
	}
	sb.WriteString(".")
	if msg := rd.Download.ErrorMsg.String; msg != "" {
		sb.WriteString("\n")
		sb.WriteString(msg)
	}
	if src := contextOrSource(rd); src != "" {
		fmt.Fprintf(&sb, "\nИсточник: %s", shorten(src, 80))
	}
	return sb.String(), b.retryKeyboard(id)
}

// retryKeyboard — клавиатура для failed/stuck: повтор + опц. ссылка в веб.
func (b *Bot) retryKeyboard(id string) *tgbotapi.InlineKeyboardMarkup {
	row := []tgbotapi.InlineKeyboardButton{
		tgbotapi.NewInlineKeyboardButtonData("🔄 Повторить", "retry:"+id),
		// Стоп-кран: закрыть зависшую задачу, не трогая файлы/раздачу.
		tgbotapi.NewInlineKeyboardButtonData("✖️ Закрыть", "dismiss:"+id),
	}
	if url := b.reviewURL(id); url != "" {
		row = append(row, tgbotapi.NewInlineKeyboardButtonURL("🌐 В вебе", url))
	}
	kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(row...))
	return &kb
}

// deletableKeyboard — клавиатура состояний, откуда доступно полное удаление
// (done/orphaned/target_missing): ссылка в веб (опц.) + «Закрыть» (стоп-кран, лишь
// статус) + «Удалить» (снос раздачи+файлов). Обе команды двухшаговые — кнопка
// ведёт на подтверждение.
func (b *Bot) deletableKeyboard(id string) *tgbotapi.InlineKeyboardMarkup {
	var row []tgbotapi.InlineKeyboardButton
	if url := b.reviewURL(id); url != "" {
		row = append(row, tgbotapi.NewInlineKeyboardButtonURL("🌐 В вебе", url))
	}
	row = append(row,
		tgbotapi.NewInlineKeyboardButtonData("✖️ Закрыть", "dismiss:"+id),
		tgbotapi.NewInlineKeyboardButtonData("🗑 Удалить", "delete:"+id),
	)
	kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(row...))
	return &kb
}

// deleteConfirmKeyboard — шаг подтверждения удаления: необратимое снятие
// раздачи+файлов и хардлинков. Явное «Да» отделено от отмены.
func (b *Bot) deleteConfirmKeyboard(id string) *tgbotapi.InlineKeyboardMarkup {
	kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData("❗ Да, удалить", "delete_confirm:"+id),
		tgbotapi.NewInlineKeyboardButtonData("Отмена", "delete_cancel:"+id),
	))
	return &kb
}

// dismissConfirmKeyboard — шаг подтверждения закрытия (стоп-кран): перевод в
// «отменено» без действий над файлами/раздачей. Явное «Да» отделено от отмены.
func (b *Bot) dismissConfirmKeyboard(id string) *tgbotapi.InlineKeyboardMarkup {
	kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData("✖️ Да, закрыть", "dismiss_confirm:"+id),
		tgbotapi.NewInlineKeyboardButtonData("Отмена", "dismiss_cancel:"+id),
	))
	return &kb
}

func (b *Bot) webOnly(id string) *tgbotapi.InlineKeyboardMarkup {
	url := b.reviewURL(id)
	if url == "" {
		return nil
	}
	kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonURL("🌐 Открыть в вебе", url),
	))
	return &kb
}

func (b *Bot) reviewURL(id string) string {
	if b.webBase == "" {
		return ""
	}
	return b.webBase + "/review/" + id
}

// --- мелкие хелперы ---

func guessLine(rd *worker.ReviewData) string {
	emoji, kind := "🎬", "фильм"
	if rd.Plan.Type == "series" {
		emoji, kind = "📺", "сериал"
	}
	title := rd.Plan.Title
	if title == "" {
		title = "не распознано"
	}
	s := fmt.Sprintf("%s %s «%s»", emoji, kind, title)
	if rd.Plan.Year != 0 {
		s += fmt.Sprintf(" (%d)", rd.Plan.Year)
	}
	return s
}

func baseLine(rec *store.Recognition) string {
	if rec == nil || !rec.Provider.Valid || rec.Provider.String == "" || rec.Provider.String == "none" {
		return "нет матча"
	}
	if rec.ProviderID.Valid && rec.ProviderID.String != "" {
		return rec.Provider.String + " " + rec.ProviderID.String
	}
	return rec.Provider.String
}

func contextOrSource(rd *worker.ReviewData) string {
	if c := strings.TrimSpace(rd.Download.Context); c != "" {
		return firstLine(c)
	}
	return rd.Download.SourceRef
}

func oppositeType(t string) string {
	if t == "series" {
		return "movie"
	}
	return "series"
}

func firstLine(s string) string {
	if i := strings.IndexByte(s, '\n'); i >= 0 {
		return s[:i]
	}
	return s
}

func tailPath(p string) string {
	dir, file := filepath.Split(p)
	parent := filepath.Base(strings.TrimRight(dir, "/"))
	if parent == "." || parent == "/" || parent == "" {
		return file
	}
	return parent + "/" + file
}

func shorten(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[:n]) + "…"
}
