package handlers

import (
	"errors"
	"fmt"
	"net/http"
	"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
	AddTagCode 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, AddTagCode: r.URL.Query().Get("code")}, "")
}

// 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{}

	data.AddTagCode = code
	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
	}

	// Photo: only change it when a NEW file is uploaded; otherwise keep the
	// tag's current photo (there is no photo_url text field anymore).
	photoURL := tag.PhotoUrl.String
	if err := r.ParseMultipartForm(10 << 20); err == nil {
		if file, header, ferr := r.FormFile("photo"); ferr == nil {
			ct := header.Header.Get("Content-Type")
			if strings.HasPrefix(ct, "image/") && a.Storage != nil {
				ext := strings.ToLower(filepath.Ext(header.Filename))
				if ext == "" {
					ext = ".jpg"
				}
				key := fmt.Sprintf("tags/%d%s", tag.ID, ext)
				if perr := a.Storage.Put(r.Context(), key, file, header.Size, ct); perr == nil {
					photoURL = "/photos/" + key
				}
			}
			file.Close()
		}
	}

	params := db.UpdateTagDetailsParams{
		ID:          tag.ID,
		ItemType:    textOrNil(strings.TrimSpace(r.FormValue("item_type"))),
		Description: textOrNil(strings.TrimSpace(r.FormValue("description"))),
		PhotoUrl:    textOrNil(photoURL),
		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 to object storage (MinIO) and
// sets its photo_url to the app's serve path (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 a.Storage == nil {
		writeUploadMsg(w, "Photo storage is not configured.")
		return
	}

	if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 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 > 10<<20 {
		writeUploadMsg(w, "Upload too large (max 5 MB).")
		return
	}
	ct := header.Header.Get("Content-Type")
	if !strings.HasPrefix(ct, "image/") {
		writeUploadMsg(w, "Only image files are allowed.")
		return
	}

	ext := strings.ToLower(filepath.Ext(header.Filename))
	if ext == "" {
		ext = ".jpg"
	}
	key := fmt.Sprintf("tags/%d%s", tag.ID, ext)
	if err := a.Storage.Put(r.Context(), key, file, header.Size, ct); err != nil {
		fmt.Println("upload to storage failed:", err)
		writeUploadMsg(w, "Could not save the photo.")
		return
	}

	photoURL := "/photos/" + key
	if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{
		ID:       tag.ID,
		PhotoUrl: pgtype.Text{String: photoURL, Valid: true},
		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")
	// Update the main form's photo_url input so a subsequent Save keeps the photo,
	// and show the thumbnail. htmx executes the inline script on swap.
	fmt.Fprintf(w, `<div><img src="%s" alt="Uploaded photo" class="mb-2 h-32 w-32 rounded-lg object-cover"><p class="text-xs text-green-700">Photo uploaded ✓</p><script>var i=document.querySelector('input[name="photo_url"]'); if(i){ i.value='%s'; }</script></div>`, photoURL, photoURL)
}

// CloseTag lets an owner close (retire) their tag. Data is kept; the tag page
// shows unavailable and alerts stop (HTMX: returns account-panel).
func (a *App) CloseTag(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	tag, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	if tag.Status == "closed" {
		a.renderAccountPanel(w, r, uid, "This tag is already closed.")
		return
	}
	if err := a.Queries.SetTagStatus(r.Context(), db.SetTagStatusParams{ID: tag.ID, Status: "closed"}); err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	a.renderAccountPanel(w, r, uid, "")
}

// MoveTag moves a tag's details to another (unset) tag code, then closes the
// source tag — the broken-tag replacement flow (HTMX: returns account-panel).
func (a *App) MoveTag(w http.ResponseWriter, r *http.Request) {
	uid, _ := auth.GetUserID(r)
	src, ok := a.loadOwnedTag(w, r, uid)
	if !ok {
		return
	}
	targetCode := strings.TrimSpace(r.FormValue("new_tag_code")) // preset IDs are case-sensitive — trim only
	if targetCode == "" {
		a.renderAccountPanel(w, r, uid, "Enter the new tag's code.")
		return
	}
	if targetCode == src.TagCode {
		a.renderAccountPanel(w, r, uid, "The new tag must be different from the current tag.")
		return
	}

	target, err := a.Queries.GetTagByCode(r.Context(), targetCode)
	if err != nil {
		a.renderAccountPanel(w, r, uid, "That tag code wasn't found. Check the code on the new tag.")
		return
	}
	if target.OwnerID.Valid || target.Status != "unset" {
		a.renderAccountPanel(w, r, uid, "That tag is already claimed or not available.")
		return
	}

	// Bind the target to this owner, copy all details, then close the source.
	if _, err := a.Queries.BindTag(r.Context(), db.BindTagParams{OwnerID: ownerID(uid), TagCode: targetCode}); err != nil {
		a.renderAccountPanel(w, r, uid, "Could not claim the new tag.")
		return
	}
	if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{
		ID:          target.ID,
		ItemType:    src.ItemType,
		Description: src.Description,
		PhotoUrl:    src.PhotoUrl,
		Phone:       src.Phone,
		Address:     src.Address,
		Notes:       src.Notes,
		SmsEnabled:  src.SmsEnabled,
	}); err != nil {
		a.renderAccountPanel(w, r, uid, "Could not copy the details.")
		return
	}
	if err := a.Queries.SetTagStatus(r.Context(), db.SetTagStatusParams{ID: src.ID, Status: "closed"}); err != nil {
		a.renderAccountPanel(w, r, uid, "Details moved, but the old tag couldn't be closed.")
		return
	}
	a.renderAccountPanel(w, r, uid, "")
}

// renderAccountPanel refreshes the My Tags panel (HTMX target) with an optional error.
func (a *App) renderAccountPanel(w http.ResponseWriter, r *http.Request, uid int64, errMsg string) {
	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, AddError: errMsg})
}

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 != ""}
}
