package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"

	"github.com/jackc/pgx/v5"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
	"git.fabledsword.com/bvandeusen/minstrel/internal/library"
)

// scanStatusResp mirrors the scan_runs row on the wire. Stage tallies are
// surfaced as json.RawMessage so the frontend gets actual JSON objects
// rather than base64 byte arrays — the DB column is bytea-shaped jsonb in
// pgx but the data is already valid JSON.
type scanStatusResp struct {
	ID              string          `json:"id"`
	StartedAt       string          `json:"started_at"`
	FinishedAt      *string         `json:"finished_at"`
	Library         json.RawMessage `json:"library,omitempty"`
	MbidBackfill    json.RawMessage `json:"mbid_backfill,omitempty"`
	CoverEnrich     json.RawMessage `json:"cover_enrich,omitempty"`
	ArtistArtEnrich json.RawMessage `json:"artist_art_enrich,omitempty"`
	ErrorMessage    *string         `json:"error_message,omitempty"`
	InFlight        bool            `json:"in_flight"`
}

// handleGetScanStatus implements GET /api/admin/scan/status.
// Returns 200 with an empty payload (no fields except in_flight=false) when
// no scan has ever run.
func (h *handlers) handleGetScanStatus(w http.ResponseWriter, r *http.Request) {
	q := dbq.New(h.pool)
	row, err := q.GetLatestScanRun(r.Context())
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeJSON(w, http.StatusOK, scanStatusResp{InFlight: false})
			return
		}
		writeErrWithLog(w, h.logger, "admin: get scan status", apierror.InternalMsg("lookup failed", err))
		return
	}

	resp := scanStatusResp{
		ID:           uuidToString(row.ID),
		StartedAt:    row.StartedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
		ErrorMessage: row.ErrorMessage,
		InFlight:     !row.FinishedAt.Valid,
	}
	if row.FinishedAt.Valid {
		s := row.FinishedAt.Time.Format("2006-01-02T15:04:05Z07:00")
		resp.FinishedAt = &s
	}
	if len(row.Library) > 0 {
		resp.Library = row.Library
	}
	if len(row.MbidBackfill) > 0 {
		resp.MbidBackfill = row.MbidBackfill
	}
	if len(row.CoverEnrich) > 0 {
		resp.CoverEnrich = row.CoverEnrich
	}
	if len(row.ArtistArtEnrich) > 0 {
		resp.ArtistArtEnrich = row.ArtistArtEnrich
	}
	writeJSON(w, http.StatusOK, resp)
}

type scanTriggerResp struct {
	ID string `json:"id,omitempty"`
}

// handleTriggerScan implements POST /api/admin/scan/run.
// Returns 202 on success, 409 if a scan is already in flight (and not stale),
// 500 on DB errors. The lazy reaper inside library.TryStartScan handles
// stale (>1h) in-flight rows automatically — the operator no longer needs
// to manually clear stuck rows after a server crash.
func (h *handlers) handleTriggerScan(w http.ResponseWriter, _ *http.Request) {
	bgCtx := context.Background()
	started, existing, err := library.TryStartScan(
		bgCtx, h.pool, h.scanner, h.coverart,
		h.logger.With("source", "manual"), h.scanCfg,
	)
	if err != nil {
		writeErrWithLog(w, h.logger, "admin: trigger scan failed", apierror.InternalMsg("trigger failed", err))
		return
	}
	if !started {
		writeJSON(w, http.StatusConflict, scanTriggerResp{ID: uuidToString(existing.ID)})
		return
	}
	writeJSON(w, http.StatusAccepted, scanTriggerResp{})
}
