package httpapi

import (
	"errors"
	"net/http"
	"strconv"

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

// --- Страница просмотра одной загрузки ---

type downloadDetailView struct {
	ID          string
	Title       string
	SourceType  string   // тип источника (magnet/torrent/url) — блок «Информация о торренте»
	SourceFull  string   // полный источник (magnet) — блок «Информация о торренте»
	Infohashes  []string // все хеши загрузки (блок «Информация о торренте»)
	Context     string
	State       string
	SelfPoll    bool // задача наблюдаема → страница сама опрашивает себя
	PollEvery   string
	Error       string
	ActionError string // ошибка действия на htmx-пути (своп download_main), не error_msg
	Note        string
	Added       string // дата добавления (source_added_at → created_at), как в списке
	AddedAgo    string // относительная давность («5 дней назад»); пусто — если не распарсить
	CreatedAt   string
	UpdatedAt   string

	// Распознавание (если есть план).
	HasPlan       bool
	MediaType     string
	IsSeries      bool
	RecTitle      string
	OriginalTitle string
	Season        string // сводка сезонов для сериала (пусто для фильма)
	Year          int
	Director      string // режиссёр эффективного источника (пусто — неизвестен)
	Provider      string
	ProviderID    string
	MatchURL      string // ссылка на запись метабазы (пусто — показываем текстом)
	NoBase        bool
	Confidence    string
	Files         []fileRow

	// Живая статистика раздачи (заполняется из снимка воркера).
	Seeding seedingView

	// Nameable — доступно ручное обновление имени: есть распознанное название,
	// которое можно перелить в display_name/ярлык раздачи. Гейтится наличием
	// распознавания (в т.ч. на done/orphaned), НЕ состоянием ревью.
	Nameable bool

	// Действия по состоянию (как на главной).
	Terminal   bool
	Reviewable bool
	Undoable   bool
	Relinkable bool
	Retriable  bool
	Deletable  bool // полное удаление доступно (done/orphaned/target_missing)
	// Dismissable — доступен стоп-кран «Закрыть» (перевод в cancelled без
	// действий над файлами/раздачей). Показываем в danger-зоне для терминальных,
	// кроме deleted (строго терминален) и cancelled (уже закрыта, no-op); у
	// не-терминальных ту же роль играет обычная «Отменить» — не дублируем.
	Dismissable bool
}

// detailTitle — заголовок страницы просмотра: имя раздачи (display_name) →
// распознанное название (план) → усечённый до одной строки сырой источник.
func detailTitle(d store.Download, rd *worker.ReviewData) string {
	if d.DisplayName != "" {
		return d.DisplayName
	}
	if rd.Plan.Title != "" {
		return rd.Plan.Title
	}
	return shorten(oneLine(d.SourceRef), 120)
}

func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
	id, err := pathID(r)
	if err != nil {
		// Невалидный id = несуществующая сущность; в БД не ходим.
		http.Error(w, "задача не найдена", http.StatusNotFound)
		return
	}
	rd, err := s.deps.Reviewer.ReviewData(r.Context(), id)
	if err != nil {
		// Тик самообновления страницы идёт этим же маршрутом (hx-get="/download/{id}"
		// с hx-select="#download-main"). Отвечать ему статусом ошибки нельзя: htmx не
		// свопит 4xx/5xx и не снимает hx-trigger — страница осталась бы навсегда
		// устаревшей, а опрос продолжался бы до закрытия вкладки. Навигационный GET
		// (адресная строка, закладка) по-прежнему получает честный статус.
		if isHTMX(r) {
			s.fragTickErr(w, err, id, "download-main")
			return
		}
		if errors.Is(err, store.ErrNotFound) {
			http.Error(w, "задача не найдена", http.StatusNotFound)
			return
		}
		s.deps.Logger.Error("download detail data failed", "download_id", id, "error", err)
		http.Error(w, "внутренняя ошибка", http.StatusInternalServerError)
		return
	}
	s.render(w, "download.html", s.buildDownloadView(id, rd))
}

// buildDownloadView собирает представление страницы загрузки из доменных данных
// и живого снимка. Общий для полной страницы (handleDownload) и htmx-свопа
// главной области после действия (renderDownloadFragment).
func (s *server) buildDownloadView(id string, rd *worker.ReviewData) downloadDetailView {
	d := rd.Download
	view := downloadDetailView{
		ID:         id,
		Title:      detailTitle(d, rd),
		SourceType: string(d.SourceType),
		SourceFull: d.SourceRef,
		Infohashes: d.HashList(),
		Context:    d.Context,
		State:      string(d.State),
		SelfPoll:   d.State.IsObservable(),
		// Блока живых цифр качания на странице нет вовсе, а тик считает
		// предпросмотр раскладки и ходит в ФС — интервал всегда медленный.
		PollEvery:  pollSlow,
		Error:      d.ErrorMsg.String,
		Note:       desyncNote(d.State),
		CreatedAt:  d.CreatedAt,
		UpdatedAt:  d.UpdatedAt,
		Terminal:   d.State.IsTerminal(),
		Reviewable: d.State == store.StateReview || d.State == store.StateDeferred,
		Undoable:   d.State == store.StateDone,
		Relinkable: d.State == store.StateReverted || d.State == store.StateCancelled ||
			d.State == store.StateTargetMissing,
		Retriable: d.State == store.StateFailed || d.State == store.StateStuck,
		Deletable: d.State.CanDelete(),
		Dismissable: d.State.IsTerminal() &&
			d.State != store.StateDeleted && d.State != store.StateCancelled,
	}
	// Дата добавления рядом с шапкой (source_added_at → фолбэк created_at,
	// как в порядке и карточках списка); неразбираемое время просто опускаем.
	if t, ok := addedTime(d); ok {
		view.Added = fmtDate(t, s.deps.Loc)
		view.AddedAgo = humanizeAge(t, store.Now())
	}

	if rd.Recognition != nil {
		view.HasPlan = len(rd.Plan.Files) > 0
		view.MediaType = string(rd.Plan.Type)
		view.IsSeries = rd.Plan.Type == "series"
		view.RecTitle = rd.Plan.Title
		view.OriginalTitle = rd.Plan.OriginalTitle
		if view.IsSeries {
			view.Season = recognize.SeasonSummary(rd.Plan)
		}
		view.Year = rd.Plan.Year
		// Режиссёр — из того же слоистого разрешения, что и display_name в шапке
		// (override → распознавание+матч → контекст), чтобы поле не расходилось с
		// заголовком; пустой во всех слоях → прочерк в шаблоне.
		view.Director = naming.EffectiveFields(d.ParsedContext, rd.Plan).Director
		// Ручное обновление имени доступно, когда есть распознанное название,
		// которое можно перелить (иначе FormatTitleYear даст пусто → no-op).
		view.Nameable = rd.Plan.Title != ""
		switch rd.Provider {
		case "", "none":
			view.NoBase = rd.Provider == "none"
		default:
			view.Provider = rd.Provider
			view.ProviderID = rd.ProviderID
			view.MatchURL = rd.MatchURL()
		}
		if rd.Recognition.Confidence.Valid {
			view.Confidence = strconv.FormatFloat(rd.Recognition.Confidence.Float64, 'f', 2, 64)
		}

		// Файл источника → целевой путь из превью (единая логика layout).
		view.Files = buildFileRows(rd.Plan, rd.Preview)
	}

	// Живая статистика раздачи — со значениями уже в первом кадре; секция
	// деградирует (пустой контейнер), если торрент не сидирует/данных нет.
	l, ok := s.liveFor(d)
	view.Seeding = buildSeeding(id, l, ok)

	return view
}
