package api

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

	"github.com/jackc/pgx/v5/pgtype"

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

// requestView is the JSON shape returned by all /api/requests handlers.
// Field names follow the lowercase snake_case convention for /api/* responses.
type requestView struct {
	ID               pgtype.UUID        `json:"id"`
	UserID           pgtype.UUID        `json:"user_id"`
	Status           string             `json:"status"`
	Kind             string             `json:"kind"`
	LidarrArtistMBID string             `json:"lidarr_artist_mbid"`
	LidarrAlbumMBID  *string            `json:"lidarr_album_mbid,omitempty"`
	LidarrTrackMBID  *string            `json:"lidarr_track_mbid,omitempty"`
	ArtistName       string             `json:"artist_name"`
	AlbumTitle       *string            `json:"album_title,omitempty"`
	TrackTitle       *string            `json:"track_title,omitempty"`
	QualityProfileID *int32             `json:"quality_profile_id,omitempty"`
	RootFolderPath   *string            `json:"root_folder_path,omitempty"`
	DecidedAt        pgtype.Timestamptz `json:"decided_at,omitempty"`
	DecidedBy        pgtype.UUID        `json:"decided_by,omitempty"`
	Notes            *string            `json:"notes,omitempty"`
	CompletedAt      pgtype.Timestamptz `json:"completed_at,omitempty"`
	MatchedTrackID   pgtype.UUID        `json:"matched_track_id,omitempty"`
	MatchedAlbumID   pgtype.UUID        `json:"matched_album_id,omitempty"`
	MatchedArtistID  pgtype.UUID        `json:"matched_artist_id,omitempty"`
	RequestedAt      pgtype.Timestamptz `json:"requested_at"`
	UpdatedAt        pgtype.Timestamptz `json:"updated_at"`
	// Progress counters, computed at response time from the matched
	// entity's children. 0 when nothing has been ingested yet (or when
	// the request has no matched entity yet). For artist requests both
	// counters fill; for album/track requests only ImportedTrackCount.
	ImportedAlbumCount int `json:"imported_album_count"`
	ImportedTrackCount int `json:"imported_track_count"`
}

func requestViewFrom(row dbq.LidarrRequest) requestView {
	return requestView{
		ID:               row.ID,
		UserID:           row.UserID,
		Status:           string(row.Status),
		Kind:             string(row.Kind),
		LidarrArtistMBID: row.LidarrArtistMbid,
		LidarrAlbumMBID:  row.LidarrAlbumMbid,
		LidarrTrackMBID:  row.LidarrTrackMbid,
		ArtistName:       row.ArtistName,
		AlbumTitle:       row.AlbumTitle,
		TrackTitle:       row.TrackTitle,
		QualityProfileID: row.QualityProfileID,
		RootFolderPath:   row.RootFolderPath,
		DecidedAt:        row.DecidedAt,
		DecidedBy:        row.DecidedBy,
		Notes:            row.Notes,
		CompletedAt:      row.CompletedAt,
		MatchedTrackID:   row.MatchedTrackID,
		MatchedAlbumID:   row.MatchedAlbumID,
		MatchedArtistID:  row.MatchedArtistID,
		RequestedAt:      row.RequestedAt,
		UpdatedAt:        row.UpdatedAt,
	}
}

// fillProgress fills ImportedAlbumCount + ImportedTrackCount on a view by
// querying the count of children for the request's matched entity.
//
// kind=artist  → counts all albums + tracks under the matched artist.
// kind=album   → counts tracks under the matched album.
// kind=track   → ImportedTrackCount = 1 once matched_track_id is set.
//
// N+1 against the request list: acceptable at admin scale (dozens of
// in-flight requests at most). Errors are swallowed — progress is a
// reporting nicety, not a correctness gate.
func fillProgress(ctx context.Context, q *dbq.Queries, row dbq.LidarrRequest, view *requestView) {
	switch row.Kind {
	case dbq.LidarrRequestKindArtist:
		if !row.MatchedArtistID.Valid {
			return
		}
		if n, err := q.CountAlbumsByArtist(ctx, row.MatchedArtistID); err == nil {
			view.ImportedAlbumCount = int(n)
		}
		if n, err := q.CountTracksByArtist(ctx, row.MatchedArtistID); err == nil {
			view.ImportedTrackCount = int(n)
		}
	case dbq.LidarrRequestKindAlbum:
		if !row.MatchedAlbumID.Valid {
			return
		}
		view.ImportedAlbumCount = 1
		if n, err := q.CountTracksByAlbum(ctx, row.MatchedAlbumID); err == nil {
			view.ImportedTrackCount = int(n)
		}
	case dbq.LidarrRequestKindTrack:
		if row.MatchedTrackID.Valid {
			view.ImportedTrackCount = 1
		}
	}
}

// createRequestBody is the decoded JSON for POST /api/requests.
type createRequestBody struct {
	Kind             string `json:"kind"`
	LidarrArtistMBID string `json:"lidarr_artist_mbid"`
	LidarrAlbumMBID  string `json:"lidarr_album_mbid"`
	LidarrTrackMBID  string `json:"lidarr_track_mbid"`
	ArtistName       string `json:"artist_name"`
	AlbumTitle       string `json:"album_title"`
	TrackTitle       string `json:"track_title"`
}

// handleCreateRequest implements POST /api/requests.
func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}

	var body createRequestBody
	if !decodeBody(w, r, &body) {
		return
	}

	row, err := h.lidarrRequests.Create(r.Context(), user.ID, lidarrrequests.CreateParams{
		Kind:             body.Kind,
		LidarrArtistMBID: body.LidarrArtistMBID,
		LidarrAlbumMBID:  body.LidarrAlbumMBID,
		LidarrTrackMBID:  body.LidarrTrackMBID,
		ArtistName:       body.ArtistName,
		AlbumTitle:       body.AlbumTitle,
		TrackTitle:       body.TrackTitle,
	})
	if err != nil {
		if errors.Is(err, lidarrrequests.ErrInvalidKindFields) {
			writeErr(w, apierror.BadRequest("mbid_required", err.Error()))
			return
		}
		h.logger.Error("api: create request", "err", err)
		writeErr(w, apierror.InternalMsg("create failed", err))
		return
	}

	// Auto-approve flow (#355 / #376 U2.5): when the user has the
	// auto_approve_requests flag set by an admin, transition the
	// just-created pending row through Approve so it dispatches to
	// Lidarr immediately. Failures here (Lidarr disabled, defaults
	// missing, network error) leave the row pending — same fallback
	// as a manual admin approve that hits the same error path. The
	// user gets a 201 with whatever status Approve produced; admin
	// can still resolve the row in /admin/requests if it stayed
	// pending.
	//
	// The "actor" id passed to Approve is the user themselves — the
	// auto-approval is the user acting under the privilege the admin
	// granted them via the toggle. The trail of "admin set the flag"
	// already lives in audit_log (ActionAutoApproveToggle from U2).
	if user.AutoApproveRequests {
		approved, aerr := h.lidarrRequests.Approve(r.Context(), row.ID, user.ID, lidarrrequests.ApproveOverrides{})
		if aerr == nil {
			row = approved
		} else {
			h.logger.Warn("api: auto-approve failed; request stays pending",
				"user_id", uuidToString(user.ID),
				"request_id", uuidToString(row.ID),
				"err", aerr)
		}
	}

	h.publishRequestStatusChanged(row)
	writeJSON(w, http.StatusCreated, requestViewFrom(row))
}

// handleListRequests implements GET /api/requests — returns caller's own requests.
func (h *handlers) handleListRequests(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}

	rows, err := h.lidarrRequests.ListForUser(r.Context(), user.ID, 100)
	if err != nil {
		h.logger.Error("api: list requests", "err", err)
		writeErr(w, apierror.InternalMsg("list failed", err))
		return
	}

	q := dbq.New(h.pool)
	out := make([]requestView, 0, len(rows))
	for _, row := range rows {
		v := requestViewFrom(row)
		fillProgress(r.Context(), q, row, &v)
		out = append(out, v)
	}
	writeJSON(w, http.StatusOK, out)
}

// handleGetRequest implements GET /api/requests/:id.
// The caller must own the request, or be an admin.
func (h *handlers) handleGetRequest(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}

	row, apiErr := resolveByID(r, "id", dbq.New(h.pool).GetLidarrRequestByID, "request")
	if apiErr != nil {
		writeErr(w, apiErr)
		return
	}

	// Allow if caller owns the request or is an admin.
	if row.UserID != user.ID && !user.IsAdmin {
		writeErr(w, apierror.NotFound("request"))
		return
	}

	v := requestViewFrom(row)
	fillProgress(r.Context(), dbq.New(h.pool), row, &v)
	writeJSON(w, http.StatusOK, v)
}

// handleCancelRequest implements DELETE /api/requests/:id.
// Cancels the caller's own pending request.
func (h *handlers) handleCancelRequest(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}

	id, ok := requireURLUUID(w, r, "id")
	if !ok {
		return
	}

	row, err := h.lidarrRequests.Cancel(r.Context(), id, user.ID)
	if err != nil {
		switch {
		case errors.Is(err, lidarrrequests.ErrNotPending):
			writeErr(w, apierror.Conflict("request_not_pending", "request is not pending"))
		case errors.Is(err, lidarrrequests.ErrNotFound):
			writeErr(w, apierror.NotFound("request"))
		default:
			h.logger.Error("api: cancel request", "err", err)
			writeErr(w, apierror.InternalMsg("cancel failed", err))
		}
		return
	}

	h.publishRequestStatusChanged(row)
	writeJSON(w, http.StatusOK, requestViewFrom(row))
}
