package docs

import (
	"encoding/json"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5"

	"gitea.parodia.dev/drwily/petal/internal/db"
)

// validTagColors is the palette a tag may use, mirrored from the design tokens.
// An unknown color on input is coerced to rose so the UI always has a token.
var validTagColors = map[string]bool{
	db.TagColorRose: true, db.TagColorMint: true, db.TagColorPeach: true,
	db.TagColorLavender: true, db.TagColorSky: true, db.TagColorHoney: true,
}

func normalizeColor(c string) string {
	if validTagColors[c] {
		return c
	}
	return db.TagColorRose
}

// TagRoutes returns the router mounted at /api/tags for tag management (the
// roster the writer creates and recolors), separate from the per-document
// assignment routes registered on the docs router.
func (h *Handler) TagRoutes() chi.Router {
	r := chi.NewRouter()
	r.Get("/", h.listTags)
	r.Post("/", h.createTag)
	r.Patch("/{id}", h.updateTag)
	r.Delete("/{id}", h.deleteTag)
	return r
}

// registerTagRoutes adds the per-document tag assignment routes onto the docs
// sub-router so they resolve under /api/docs/{id}/tags.
func (h *Handler) registerTagRoutes(r chi.Router) {
	r.Post("/{id}/tags", h.assignTag)
	r.Delete("/{id}/tags/{tagId}", h.unassignTag)
}

// listTags returns the user's tags alphabetically, each with the number of
// documents that wear it (so the sidebar can show counts and hide empties later
// if desired).
func (h *Handler) listTags(w http.ResponseWriter, r *http.Request) {
	rows, err := h.DB.Query(
		`SELECT t.id, t.name, t.color, COUNT(dt.doc_id)
		   FROM tags t
		   LEFT JOIN document_tags dt ON dt.tag_id = t.id
		  WHERE t.user_id = ?
		  GROUP BY t.id
		  ORDER BY t.name COLLATE NOCASE`,
		db.LocalUserID,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	defer rows.Close()

	out := []db.Tag{} // non-nil so an empty roster serializes as []
	for rows.Next() {
		var t db.Tag
		if err := rows.Scan(&t.ID, &t.Name, &t.Color, &t.DocCount); err != nil {
			serverError(w, err)
			return
		}
		out = append(out, t)
	}
	if err := rows.Err(); err != nil {
		serverError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, out)
}

type tagRequest struct {
	Name  string `json:"name"`
	Color string `json:"color"`
}

// createTag adds a tag. It's idempotent on (user, name): re-creating an existing
// tag returns it unchanged rather than erroring, so the client can "create or
// reuse" in one call. Recoloring is done via updateTag.
func (h *Handler) createTag(w http.ResponseWriter, r *http.Request) {
	var req tagRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON body")
		return
	}
	name := strings.TrimSpace(req.Name)
	if name == "" {
		badRequest(w, "tag name is required")
		return
	}

	var t db.Tag
	err := h.DB.QueryRow(
		`INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)
		 ON CONFLICT(user_id, name) DO UPDATE SET name = excluded.name
		 RETURNING id, name, color`,
		db.LocalUserID, name, normalizeColor(req.Color),
	).Scan(&t.ID, &t.Name, &t.Color)
	if err != nil {
		serverError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, t)
}

// updateTag renames and/or recolors a tag. Both fields optional via pointers so
// a recolor needn't resend the name.
func (h *Handler) updateTag(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	var req struct {
		Name  *string `json:"name"`
		Color *string `json:"color"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON body")
		return
	}

	var namePtr, colorPtr any
	if req.Name != nil {
		n := strings.TrimSpace(*req.Name)
		if n == "" {
			badRequest(w, "tag name cannot be empty")
			return
		}
		namePtr = n
	}
	if req.Color != nil {
		colorPtr = normalizeColor(*req.Color)
	}

	res, err := h.DB.Exec(
		`UPDATE tags
		    SET name  = COALESCE(?, name),
		        color = COALESCE(?, color)
		  WHERE id = ? AND user_id = ?`,
		namePtr, colorPtr, id, db.LocalUserID,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		notFoundMsg(w, "tag not found")
		return
	}

	var t db.Tag
	if err := h.DB.QueryRow(
		`SELECT id, name, color FROM tags WHERE id = ? AND user_id = ?`,
		id, db.LocalUserID,
	).Scan(&t.ID, &t.Name, &t.Color); err != nil {
		serverError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, t)
}

// deleteTag removes a tag; its document assignments cascade away via the FK.
func (h *Handler) deleteTag(w http.ResponseWriter, r *http.Request) {
	res, err := h.DB.Exec(
		`DELETE FROM tags WHERE id = ? AND user_id = ?`,
		chi.URLParam(r, "id"), db.LocalUserID,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		notFoundMsg(w, "tag not found")
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// assignTag attaches a tag to a document. Both must belong to the local user;
// the assignment is idempotent (re-assigning is a no-op, not an error).
func (h *Handler) assignTag(w http.ResponseWriter, r *http.Request) {
	docID := chi.URLParam(r, "id")

	var req struct {
		TagID string `json:"tag_id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON body")
		return
	}
	if req.TagID == "" {
		badRequest(w, "tag_id is required")
		return
	}

	// Verify both the doc and the tag belong to the user before linking, so a
	// stray id can't cross-link another account's rows.
	if !h.ownsDoc(docID) {
		notFound(w)
		return
	}
	if !h.ownsTag(req.TagID) {
		notFoundMsg(w, "tag not found")
		return
	}

	if _, err := h.DB.Exec(
		`INSERT INTO document_tags (doc_id, tag_id) VALUES (?, ?)
		 ON CONFLICT(doc_id, tag_id) DO NOTHING`,
		docID, req.TagID,
	); err != nil {
		serverError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// unassignTag detaches a tag from a document.
func (h *Handler) unassignTag(w http.ResponseWriter, r *http.Request) {
	docID := chi.URLParam(r, "id")
	tagID := chi.URLParam(r, "tagId")

	if !h.ownsDoc(docID) {
		notFound(w)
		return
	}
	if _, err := h.DB.Exec(
		`DELETE FROM document_tags WHERE doc_id = ? AND tag_id = ?`,
		docID, tagID,
	); err != nil {
		serverError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ownsDoc reports whether a document belongs to the local user.
func (h *Handler) ownsDoc(docID string) bool {
	var exists bool
	_ = h.DB.QueryRow(
		`SELECT EXISTS(SELECT 1 FROM documents WHERE id = ? AND user_id = ?)`,
		docID, db.LocalUserID,
	).Scan(&exists)
	return exists
}

// ownsTag reports whether a tag belongs to the local user.
func (h *Handler) ownsTag(tagID string) bool {
	var exists bool
	_ = h.DB.QueryRow(
		`SELECT EXISTS(SELECT 1 FROM tags WHERE id = ? AND user_id = ?)`,
		tagID, db.LocalUserID,
	).Scan(&exists)
	return exists
}

// tagsByDoc loads the tags for a set of documents in one query and groups them
// by doc id. Used to decorate the document list and search results without an
// N+1 of per-doc queries. Returns an empty (non-nil) map when ids is empty.
func (h *Handler) tagsByDoc(ids []string) (map[string][]db.Tag, error) {
	out := map[string][]db.Tag{}
	if len(ids) == 0 {
		return out, nil
	}

	// Build the IN (?, ?, …) placeholder list.
	ph := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",")
	args := make([]any, 0, len(ids)+1)
	for _, id := range ids {
		args = append(args, id)
	}
	args = append(args, db.LocalUserID)

	rows, err := h.DB.Query(
		`SELECT dt.doc_id, t.id, t.name, t.color
		   FROM document_tags dt
		   JOIN tags t ON t.id = dt.tag_id
		  WHERE dt.doc_id IN (`+ph+`) AND t.user_id = ?
		  ORDER BY t.name COLLATE NOCASE`,
		args...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var docID string
		var t db.Tag
		if err := rows.Scan(&docID, &t.ID, &t.Name, &t.Color); err != nil {
			return nil, err
		}
		out[docID] = append(out[docID], t)
	}
	return out, rows.Err()
}
