// Package docs implements the document CRUD HTTP handlers — the create / list /
// read / update / delete surface that backs the editor and its 1.5s auto-save.
// All access is scoped to the single hardcoded local user while auth is deferred.
package docs

import (
	"database/sql"
	"encoding/json"
	"errors"
	"log"
	"net/http"

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

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

// Handler holds the dependencies shared by every document route.
type Handler struct {
	DB *db.DB
}

// New constructs a Handler.
func New(database *db.DB) *Handler {
	return &Handler{DB: database}
}

// Routes returns a router mounting the document CRUD endpoints. Mount it under
// "/docs" so the full paths are /api/docs, /api/docs/{id}, etc.
func (h *Handler) Routes() chi.Router {
	r := chi.NewRouter()
	r.Get("/", h.list)
	r.Post("/", h.create)
	r.Get("/{id}", h.get)
	r.Put("/{id}", h.update)
	r.Delete("/{id}", h.delete)
	h.versionRoutes(r)
	h.exportRoutes(r)
	h.registerTagRoutes(r)
	return r
}

// docSummary is the lightweight shape returned by the list endpoint — enough to
// render the DocList sidebar without shipping every document's full body. Tags
// ride along so the sidebar can show chips and filter without a second request.
type docSummary struct {
	ID        string   `json:"id"`
	Title     string   `json:"title"`
	WordCount int      `json:"word_count"`
	UpdatedAt string   `json:"updated_at"`
	Tags      []db.Tag `json:"tags"`
}

// list returns the local user's documents, most-recently-updated first, each
// decorated with its tags.
func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
	rows, err := h.DB.Query(
		`SELECT id, title, word_count, updated_at
		   FROM documents
		  WHERE user_id = ?
		  ORDER BY updated_at DESC`,
		db.LocalUserID,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	defer rows.Close()

	out := []docSummary{} // non-nil so an empty list serializes as [] not null
	ids := []string{}
	for rows.Next() {
		var d docSummary
		if err := rows.Scan(&d.ID, &d.Title, &d.WordCount, &d.UpdatedAt); err != nil {
			serverError(w, err)
			return
		}
		out = append(out, d)
		ids = append(ids, d.ID)
	}
	if err := rows.Err(); err != nil {
		serverError(w, err)
		return
	}

	byDoc, err := h.tagsByDoc(ids)
	if err != nil {
		serverError(w, err)
		return
	}
	for i := range out {
		out[i].Tags = byDoc[out[i].ID] // nil → JSON null is fine; client treats as none
		if out[i].Tags == nil {
			out[i].Tags = []db.Tag{}
		}
	}
	writeJSON(w, http.StatusOK, out)
}

// create inserts a fresh blank document and returns it in full.
func (h *Handler) create(w http.ResponseWriter, r *http.Request) {
	var doc db.Document
	err := h.DB.QueryRow(
		`INSERT INTO documents (user_id) VALUES (?)
		 RETURNING id, user_id, title, content, content_text, tone, word_count, created_at, updated_at`,
		db.LocalUserID,
	).Scan(
		&doc.ID, &doc.UserID, &doc.Title, &doc.Content, &doc.ContentText,
		&doc.Tone, &doc.WordCount, &doc.CreatedAt, &doc.UpdatedAt,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, doc)
}

// get returns a single full document by id.
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
	doc, err := h.fetch(chi.URLParam(r, "id"))
	if errors.Is(err, sql.ErrNoRows) {
		notFound(w)
		return
	}
	if err != nil {
		serverError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, doc)
}

// updateRequest is the auto-save payload. Every field is optional (a pointer) so
// the same endpoint serves both the editor's full save ({content, content_text,
// word_count, title}) and a DocList rename ({title} alone).
type updateRequest struct {
	Title       *string `json:"title"`
	Content     *string `json:"content"`
	ContentText *string `json:"content_text"`
	Tone        *string `json:"tone"`
	WordCount   *int    `json:"word_count"`
}

// update applies the provided fields to a document and returns the saved row.
// content and content_text are kept in sync by the client and written together.
func (h *Handler) update(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	var req updateRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON body")
		return
	}

	res, err := h.DB.Exec(
		`UPDATE documents
		    SET title        = COALESCE(?, title),
		        content      = COALESCE(?, content),
		        content_text = COALESCE(?, content_text),
		        tone         = COALESCE(?, tone),
		        word_count   = COALESCE(?, word_count),
		        updated_at   = CURRENT_TIMESTAMP
		  WHERE id = ? AND user_id = ?`,
		req.Title, req.Content, req.ContentText, req.Tone, req.WordCount, id, db.LocalUserID,
	)
	if err != nil {
		serverError(w, err)
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		notFound(w)
		return
	}

	doc, err := h.fetch(id)
	if err != nil {
		serverError(w, err)
		return
	}

	// Capture a throttled history snapshot when a real body save came through
	// (not a bare rename) and the document has content. Best-effort: a failed
	// snapshot must never fail the save itself.
	if req.Content != nil && doc.ContentText != "" {
		if err := h.maybeAutoSnapshot(doc); err != nil {
			log.Printf("docs: auto-snapshot for %s failed: %v", id, err)
		}
	}

	writeJSON(w, http.StatusOK, doc)
}

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

// fetch loads one full document scoped to the local user.
func (h *Handler) fetch(id string) (db.Document, error) {
	var doc db.Document
	err := h.DB.QueryRow(
		`SELECT id, user_id, title, content, content_text, tone, word_count, created_at, updated_at
		   FROM documents
		  WHERE id = ? AND user_id = ?`,
		id, db.LocalUserID,
	).Scan(
		&doc.ID, &doc.UserID, &doc.Title, &doc.Content, &doc.ContentText,
		&doc.Tone, &doc.WordCount, &doc.CreatedAt, &doc.UpdatedAt,
	)
	return doc, err
}

// --- small response helpers -------------------------------------------------

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func errorJSON(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}

func serverError(w http.ResponseWriter, err error) {
	errorJSON(w, http.StatusInternalServerError, err.Error())
}

func badRequest(w http.ResponseWriter, msg string) { errorJSON(w, http.StatusBadRequest, msg) }
func notFound(w http.ResponseWriter)               { errorJSON(w, http.StatusNotFound, "document not found") }
func notFoundMsg(w http.ResponseWriter, msg string) { errorJSON(w, http.StatusNotFound, msg) }
