package api

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"

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

// inviteTTL is the lifetime of a freshly-minted invite. 24 hours
// matches the spec; admin can revoke an unredeemed invite at any
// time anyway, so a short TTL is just defense-in-depth against
// shared-link leakage.
const inviteTTL = 24 * time.Hour

type inviteResp struct {
	Token      string  `json:"token"`
	InvitedBy  string  `json:"invited_by"`
	Note       *string `json:"note"`
	CreatedAt  string  `json:"created_at"`
	ExpiresAt  string  `json:"expires_at"`
	RedeemedAt *string `json:"redeemed_at"`
	RedeemedBy *string `json:"redeemed_by"`
}

type listInvitesResp struct {
	Invites []inviteResp `json:"invites"`
}

func (h *handlers) handleListInvites(w http.ResponseWriter, r *http.Request) {
	q := dbq.New(h.pool)
	rows, err := q.ListInvitesActive(r.Context())
	if err != nil {
		writeErrWithLog(w, h.logger, "admin: list invites failed", apierror.Internal(err))
		return
	}
	out := make([]inviteResp, 0, len(rows))
	for _, row := range rows {
		out = append(out, inviteFromRow(row))
	}
	writeJSON(w, http.StatusOK, listInvitesResp{Invites: out})
}

type createInviteReq struct {
	Note string `json:"note"`
}

func (h *handlers) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	var req createInviteReq
	// Body is optional — empty body is fine; ignore decode errors.
	if r.Body != nil && r.ContentLength != 0 {
		_ = json.NewDecoder(r.Body).Decode(&req)
	}

	tokenBytes := make([]byte, 16)
	if _, err := rand.Read(tokenBytes); err != nil {
		writeErrWithLog(w, h.logger, "admin: invite token gen failed", apierror.Internal(err))
		return
	}
	token := hex.EncodeToString(tokenBytes)
	expiresAt := time.Now().Add(inviteTTL)

	q := dbq.New(h.pool)
	var notePtr *string
	if req.Note != "" {
		n := req.Note
		notePtr = &n
	}
	row, err := q.CreateInvite(r.Context(), dbq.CreateInviteParams{
		Token:     token,
		InvitedBy: user.ID,
		Note:      notePtr,
		ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true},
	})
	if err != nil {
		writeErrWithLog(w, h.logger, "admin: create invite failed", apierror.Internal(err))
		return
	}

	audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, pgtype.UUID{}, audit.ActionInviteCreate,
		map[string]any{"token": token})

	writeJSON(w, http.StatusOK, inviteFromRow(row))
}

func (h *handlers) handleDeleteInvite(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	token := chi.URLParam(r, "token")
	if token == "" {
		writeErr(w, apierror.BadRequest("invalid_token", ""))
		return
	}
	q := dbq.New(h.pool)
	// Verify the invite exists + is unredeemed before deleting, so we can
	// return a useful 404 vs 200.
	if _, err := q.GetInviteByToken(r.Context(), token); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: ""})
			return
		}
		writeErrWithLog(w, h.logger, "admin: get invite failed", apierror.Internal(err))
		return
	}
	if err := q.DeleteInvite(r.Context(), token); err != nil {
		writeErrWithLog(w, h.logger, "admin: delete invite failed", apierror.Internal(err))
		return
	}
	audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, pgtype.UUID{}, audit.ActionInviteRevoke,
		map[string]any{"token": token})
	w.WriteHeader(http.StatusNoContent)
}

func inviteFromRow(row dbq.UserInvite) inviteResp {
	resp := inviteResp{
		Token:     row.Token,
		InvitedBy: uuidToString(row.InvitedBy),
		Note:      row.Note,
		CreatedAt: row.CreatedAt.Time.UTC().Format(time.RFC3339),
		ExpiresAt: row.ExpiresAt.Time.UTC().Format(time.RFC3339),
	}
	if row.RedeemedAt.Valid {
		s := row.RedeemedAt.Time.UTC().Format(time.RFC3339)
		resp.RedeemedAt = &s
	}
	if row.RedeemedBy.Valid {
		s := uuidToString(row.RedeemedBy)
		resp.RedeemedBy = &s
	}
	return resp
}
