package handlers

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

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

	"wherewoof/frontend/internal/auth"
	"wherewoof/frontend/internal/db"
)

const maxTagsPerAccount = 20

type accountData struct {
	Tags     []db.Tag
	AddError string
}

func ownerID(uid int64) pgtype.Int8 {
	return pgtype.Int8{Int64: uid, Valid: true}
}

func (a *App) Account(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tags, err := a.Queries.ListTagsByOwner(r.Context(), ownerID(uid))
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	a.render(w, r, "account", "My Tags", accountData{Tags: tags}, "")
}

// AddTag binds a tag code to the current account (HTMX: returns account-panel).
func (a *App) AddTag(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	// Tag codes are case-sensitive (preset base64 IDs) — trim only, never normalise case.
	code := strings.TrimSpace(r.FormValue("tag_code"))
	oid := ownerID(uid)
	data := accountData{}

	if code == "" {
		data.AddError = "Enter a tag code."
	} else {
		cnt, err := a.Queries.CountTagsByOwner(r.Context(), oid)
		if err != nil {
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		if cnt >= maxTagsPerAccount {
			data.AddError = "Limit reached: each account can hold 20 tags."
		} else {
			_, err := a.Queries.BindTag(r.Context(), db.BindTagParams{OwnerID: oid, TagCode: code})
			if err != nil {
				if errors.Is(err, pgx.ErrNoRows) {
					data.AddError = "Tag not found, or already claimed by another account."
				} else {
					http.Error(w, "internal error", http.StatusInternalServerError)
					return
				}
			}
		}
	}

	tags, err := a.Queries.ListTagsByOwner(r.Context(), oid)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	data.Tags = tags
	a.renderPartial(w, "account", "account-panel", data)
}

func (a *App) EditTagPage(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	if tag.Status == "suspended" {
		a.render(w, r, "edit", "Edit tag", tag, "This tag is suspended and cannot be edited.")
		return
	}
	a.render(w, r, "edit", "Edit tag", tag, "")
}

// EditInlinePage returns the compact inline edit form for HTMX row editing.
func (a *App) EditInlinePage(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	a.renderPartial(w, "account", "tag-edit-inline", tag)
}

func (a *App) EditTag(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	if tag.Status == "suspended" {
		a.render(w, r, "edit", "Edit tag", tag, "This tag is suspended and cannot be edited.")
		return
	}

	params := db.UpdateTagDetailsParams{
		ID:          tag.ID,
		ItemType:    textOrNil(strings.TrimSpace(r.FormValue("item_type"))),
		Description: textOrNil(strings.TrimSpace(r.FormValue("description"))),
		PhotoUrl:    textOrNil(strings.TrimSpace(r.FormValue("photo_url"))),
		Phone:       textOrNil(strings.TrimSpace(r.FormValue("phone"))),
		Address:     textOrNil(strings.TrimSpace(r.FormValue("address"))),
		Notes:       textOrNil(strings.TrimSpace(r.FormValue("notes"))),
		SmsEnabled:  r.FormValue("sms_enabled") == "on",
	}
	if _, err := a.Queries.UpdateTagDetails(r.Context(), params); err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	// If this was an HTMX inline edit, return the refreshed account panel.
	if r.Header.Get("HX-Request") == "true" {
		tags, err := a.Queries.ListTagsByOwner(r.Context(), ownerID(uid))
		if err != nil {
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		a.renderPartial(w, "account", "account-panel", accountData{Tags: tags})
		return
	}
	http.Redirect(w, r, "/account", http.StatusSeeOther)
}

// DeleteTag reverts a tag to unset so it can be re-bound (HTMX: account-panel).
func (a *App) DeleteTag(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	if _, err := a.Queries.ClearTagOwner(r.Context(), tag.ID); err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	tags, err := a.Queries.ListTagsByOwner(r.Context(), ownerID(uid))
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	a.renderPartial(w, "account", "account-panel", accountData{Tags: tags})
}

// loadOwnedTag fetches a tag by path id and verifies it belongs to uid.
func (a *App) loadOwnedTag(w http.ResponseWriter, r *http.Request, uid int64) (db.Tag, bool) {
	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return db.Tag{}, false
	}
	tag, err := a.Queries.GetTagByID(r.Context(), id)
	if err != nil || !tag.OwnerID.Valid || tag.OwnerID.Int64 != uid {
		http.NotFound(w, r)
		return db.Tag{}, false
	}
	return tag, true
}

// UploadPhoto saves an uploaded image for a tag and sets its photo_url (HTMX).
func (a *App) UploadPhoto(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}

	if err := r.ParseMultipartForm(5 << 20); err != nil { // 5 MB
		writeUploadMsg(w, "Upload too large (max 5 MB).")
		return
	}
	file, header, err := r.FormFile("photo")
	if err != nil {
		writeUploadMsg(w, "Please choose an image file.")
		return
	}
	defer file.Close()

	if header.Size > 5<<20 {
		writeUploadMsg(w, "Upload too large (max 5 MB).")
		return
	}
	if !strings.HasPrefix(header.Header.Get("Content-Type"), "image/") {
		writeUploadMsg(w, "Only image files are allowed.")
		return
	}

	// Sanitise the filename and store under uploads/.
	ext := strings.ToLower(filepath.Ext(header.Filename))
	if ext == "" {
		ext = ".jpg"
	}
	dst := filepath.Join("static", "uploads", fmt.Sprintf("tag%d%s", tag.ID, ext))
	out, err := os.Create(dst)
	if err != nil {
		writeUploadMsg(w, "Could not save the photo.")
		return
	}
	defer out.Close()
	if _, err := io.Copy(out, file); err != nil {
		writeUploadMsg(w, "Could not save the photo.")
		return
	}

	photoURL := "/static/uploads/" + filepath.Base(dst)
	if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{
		ID:       tag.ID,
		PhotoUrl: pgtype.Text{String: photoURL, Valid: true},
		// Preserve existing values: re-send them via the form is not possible here,
		// so use the stored tag fields for everything else.
		ItemType: tag.ItemType, Description: tag.Description, Phone: tag.Phone,
		Address: tag.Address, Notes: tag.Notes, SmsEnabled: tag.SmsEnabled,
	}); err != nil {
		writeUploadMsg(w, "Could not update the tag.")
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, `<p class="mt-2 text-xs text-green-700">Photo uploaded ✓ <a href="%s" class="underline">view</a> — refresh to see it on the tag page.</p>`, photoURL)
}

func writeUploadMsg(w http.ResponseWriter, msg string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, `<p class="mt-2 text-xs text-red-600">%s</p>`, msg)
}

func textOrNil(s string) pgtype.Text {
	return pgtype.Text{String: s, Valid: s != ""}
}
