//go:build !headless && !lite

package admin

import (
	"errors"
	"math"
	"net/http"
	"net/mail"
	"strconv"
	"strings"

	"breadbox/internal/app"
	"breadbox/internal/avatar"
	"breadbox/internal/db"
	"breadbox/internal/pgconv"
	"breadbox/internal/templates/components"
	"breadbox/internal/templates/components/pages"

	"github.com/alexedwards/scs/v2"
	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
)

// UserAccountSummary holds a single account's display info for the users page.
type UserAccountSummary struct {
	ID              string
	Name            string
	Type            string
	Subtype         string
	Mask            string
	InstitutionName string
	BalanceCurrent  float64
	IsoCurrencyCode string
	IsLiability      bool
	HasBalance       bool
	ConnectionStatus string // active, error, pending_reauth, disconnected
}

// EnrichedUser holds a user plus their computed financial summary.
type EnrichedUser struct {
	db.User
	Accounts         []UserAccountSummary
	ConnectionCount  int64
	AccountCount     int
	TotalAssets      float64
	TotalLiabilities float64
	NetWorth         float64
}

// UsersListHandler serves GET /admin/users.
func UsersListHandler(a *app.App, sm *scs.SessionManager, tr *TemplateRenderer) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		users, err := a.Queries.ListUsers(ctx)
		if err != nil {
			a.Logger.Error("list users", "error", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		// Enrich each user with account data and financial summary.
		enrichedUsers := make([]EnrichedUser, 0, len(users))
		for _, u := range users {
			eu := EnrichedUser{
				User: u,
			}

			accounts, err := a.Queries.ListAccountsByUser(ctx, u.ID)
			if err != nil {
				a.Logger.Error("list accounts for user", "error", err, "user_id", pgconv.FormatUUID(u.ID))
			} else {
				eu.AccountCount = len(accounts)
				// Count distinct connections from displayed accounts so the
				// count matches regardless of connection status.
				connSet := make(map[string]struct{})
				for _, acct := range accounts {
					if acct.ConnectionID.Valid {
						connSet[pgconv.FormatUUID(acct.ConnectionID)] = struct{}{}
					}
				}
				eu.ConnectionCount = int64(len(connSet))
				for _, acct := range accounts {
					bal, hasBal := pgconv.NumericToFloat(acct.BalanceCurrent)
					// Clean up subtype for display (e.g. "credit_card" -> "Credit Card")
					subtype := strings.ReplaceAll(pgconv.TextOr(acct.Subtype, ""), "_", " ")
					mask := pgconv.TextOr(acct.Mask, "")
					institution := pgconv.TextOr(acct.InstitutionName, "")
					currency := pgconv.TextOr(acct.IsoCurrencyCode, "USD")
					displayName := pgconv.TextOr(acct.DisplayName, acct.Name)

					isLiability := IsLiabilityAccount(acct.Type)
					displayBal := bal
					if hasBal {
						if isLiability {
							eu.TotalLiabilities += math.Abs(bal)
							eu.NetWorth -= math.Abs(bal)
							// Show liabilities as negative in the UI
							displayBal = -math.Abs(bal)
						} else {
							eu.TotalAssets += bal
							eu.NetWorth += bal
						}
					}

					connStatus := string(acct.ConnectionStatus)
					eu.Accounts = append(eu.Accounts, UserAccountSummary{
						ID:               pgconv.FormatUUID(acct.ID),
						Name:             displayName,
						Type:             acct.Type,
						Subtype:          subtype,
						Mask:             mask,
						InstitutionName:  institution,
						BalanceCurrent:   displayBal,
						IsoCurrencyCode:  currency,
						IsLiability:      isLiability,
						HasBalance:       hasBal,
						ConnectionStatus: connStatus,
					})
				}
			}

			enrichedUsers = append(enrichedUsers, eu)
		}

		// Load login accounts (auth_accounts with user_id).
		loginAccounts, err := a.Queries.ListAuthAccountsWithUser(ctx)
		if err != nil {
			a.Logger.Error("list login accounts", "error", err)
		}
		// Build a map of user_id -> login account for quick lookup in template.
		loginAccountMap := make(map[string]db.ListAuthAccountsWithUserRow)
		for _, la := range loginAccounts {
			loginAccountMap[pgconv.FormatUUID(la.UserID)] = la
		}

		// Check if the current admin is unlinked (no household member).
		userIDStr := SessionUserID(sm, r)
		isUnlinked := userIDStr == ""
		var unlinkedUsers []db.User
		if isUnlinked {
			if uu, err := a.Queries.ListUsersWithoutAuthAccount(ctx); err == nil {
				unlinkedUsers = uu
			}
		}

		props := buildUsersProps(usersListInput{
			CSRFToken:       GetCSRFToken(r),
			Created:         r.URL.Query().Get("created") == "1",
			IsUnlinked:      isUnlinked,
			UnlinkedUsers:   unlinkedUsers,
			EnrichedUsers:   enrichedUsers,
			LoginAccountMap: loginAccountMap,
		})
		data := BaseTemplateData(r, sm, "household", "Household")
		renderUsersList(tr, w, r, data, props)
	}
}

// renderUserForm renders the create/edit user form via the templ component.
// Both NewUserHandler and EditUserHandler funnel through here so the page
// title, breadcrumbs, and IsEdit flag are derived in one place.
//
// Household pages are top-level (not inside the Settings modal) — they
// render directly through tr.RenderWithTempl so the standard sidebar +
// drawer chrome wrap them.
func renderUserForm(sm *scs.SessionManager, tr *TemplateRenderer, w http.ResponseWriter, r *http.Request, props pages.UserFormProps) {
	pageTitle := "Add Family Member"
	if props.IsEdit && props.User != nil {
		pageTitle = "Edit " + props.User.Name
	}
	data := BaseTemplateData(r, sm, "household", pageTitle)
	crumbs := []components.Breadcrumb{{Label: "Household", Href: "/household"}}
	if props.IsEdit && props.User != nil {
		crumbs = append(crumbs, components.Breadcrumb{Label: props.User.Name})
	} else {
		crumbs = append(crumbs, components.Breadcrumb{Label: "Add Member"})
	}
	data["Breadcrumbs"] = crumbs
	tr.RenderWithTempl(w, r, data, pages.UserForm(props))
}

// renderUsersList drives the household-members page through the templ
// component as a standalone top-level page (no Settings shell).
func renderUsersList(tr *TemplateRenderer, w http.ResponseWriter, r *http.Request, data map[string]any, props pages.UsersProps) {
	tr.RenderWithTempl(w, r, data, pages.Users(props))
}

// usersListInput collects the values the handler computes for the list
// page. Building props through this struct keeps the conversion from
// db rows + admin view-models into typed templ props in one place.
type usersListInput struct {
	CSRFToken       string
	Created         bool
	IsUnlinked      bool
	UnlinkedUsers   []db.User
	EnrichedUsers   []EnrichedUser
	LoginAccountMap map[string]db.ListAuthAccountsWithUserRow
}

// buildUsersProps converts the handler's enriched-user/login data into
// the typed pages.UsersProps the templ component renders.
func buildUsersProps(in usersListInput) pages.UsersProps {
	props := pages.UsersProps{
		CSRFToken:  in.CSRFToken,
		Created:    in.Created,
		IsUnlinked: in.IsUnlinked,
	}

	for _, u := range in.UnlinkedUsers {
		props.UnlinkedUsers = append(props.UnlinkedUsers, pages.UsersUnlinkedRow{
			ID:   pgconv.FormatUUID(u.ID),
			Name: u.Name,
		})
	}

	for _, eu := range in.EnrichedUsers {
		uid := pgconv.FormatUUID(eu.ID)
		row := pages.UsersEnrichedRow{
			ID:              uid,
			Name:            eu.Name,
			Email:           pgconv.TextOr(eu.Email, ""),
			HasEmail:        eu.Email.Valid,
			AvatarVersion:   usersAvatarVersion(eu.UpdatedAt),
			AccountCount:    eu.AccountCount,
			ConnectionCount: eu.ConnectionCount,
		}
		if eu.CreatedAt.Valid {
			row.HasCreatedAt = true
			row.CreatedAtLabel = eu.CreatedAt.Time.Format("Jan 2006")
		}
		if la, ok := in.LoginAccountMap[uid]; ok && la.Username != "" {
			row.HasLogin = true
			row.LoginRole = la.Role
			row.LoginUsername = la.Username
			row.LoginSetupPending = len(la.HashedPassword) == 0
		}
		props.EnrichedUsers = append(props.EnrichedUsers, row)
	}

	return props
}

// usersAvatarVersion returns the per-user cache-bust suffix (the
// updated_at unix timestamp as a string) for components.UserAvatar's
// Version prop. URL construction itself lives inside the shared
// component (components.AvatarURLWith) so all callers stay aligned.
func usersAvatarVersion(updatedAt pgtype.Timestamptz) string {
	if !updatedAt.Valid {
		return ""
	}
	return strconv.FormatInt(updatedAt.Time.Unix(), 10)
}

// NewUserHandler serves GET /admin/users/new.
func NewUserHandler(a *app.App, sm *scs.SessionManager, tr *TemplateRenderer) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		renderUserForm(sm, tr, w, r, pages.UserFormProps{
			IsEdit: false,
		})
	}
}

// EditUserHandler serves GET /admin/users/{id}/edit.
func EditUserHandler(a *app.App, sm *scs.SessionManager, tr *TemplateRenderer) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		userID, ok := parseURLUUIDOrNotFound(w, r, tr, "id")
		if !ok {
			return
		}
		idStr := chi.URLParam(r, "id")

		user, err := a.Queries.GetUser(ctx, userID)
		if err != nil {
			a.Logger.Error("get user", "error", err)
			tr.RenderNotFound(w, r)
			return
		}

		renderUserForm(sm, tr, w, r, pages.UserFormProps{
			IsEdit: true,
			User:   &user,
			UserID: idStr,
		})
	}
}

// createUserRequest is the JSON body for POST /admin/api/users.
type createUserRequest struct {
	Name  string  `json:"name"`
	Email *string `json:"email,omitempty"`
}

// CreateUserHandler serves POST /admin/api/users.
func CreateUserHandler(a *app.App, sm *scs.SessionManager) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req createUserRequest
		if !decodeJSON(w, r, &req) {
			return
		}

		req.Name = strings.TrimSpace(req.Name)
		if req.Name == "" {
			writeError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Name is required")
			return
		}

		var emailText pgtype.Text
		if req.Email != nil && *req.Email != "" {
			if _, err := mail.ParseAddress(*req.Email); err != nil {
				writeError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Invalid email format")
				return
			}
			emailText = pgconv.Text(*req.Email)
		}

		user, err := a.Queries.CreateUser(r.Context(), db.CreateUserParams{
			Name:  req.Name,
			Email: emailText,
		})
		if err != nil {
			var pgErr *pgconn.PgError
			if errors.As(err, &pgErr) && pgErr.Code == "23505" {
				writeError(w, http.StatusConflict, "DUPLICATE_EMAIL", "A family member with this email already exists")
				return
			}
			a.Logger.Error("create user", "error", err)
			writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to create user")
			return
		}

		userID := pgconv.FormatUUID(user.ID)

		writeJSON(w, http.StatusCreated, map[string]any{
			"id":         userID,
			"name":       user.Name,
			"email":      pgconv.TextPtr(user.Email),
			"created_at": user.CreatedAt.Time,
			"updated_at": user.UpdatedAt.Time,
		})
	}
}

// updateUserRequest is the JSON body for PUT /admin/api/users/{id} and
// the self-service PUT /settings/account/profile. AvatarSeed is the
// "pending" identicon seed staged client-side by the Regenerate
// button — handlers apply it via clearAvatarUploadAndSetSeed so the
// uploaded image (if any) is dropped and the new generated identicon
// takes its place. Empty / omitted leaves the avatar unchanged.
type updateUserRequest struct {
	Name       *string `json:"name,omitempty"`
	Email      *string `json:"email"`
	AvatarSeed *string `json:"avatar_seed,omitempty"`
}

// UpdateUserHandler serves PUT /admin/api/users/{id}.
func UpdateUserHandler(a *app.App, sm *scs.SessionManager) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := chi.URLParam(r, "id")

		var userID pgtype.UUID
		if err := userID.Scan(idStr); err != nil {
			writeError(w, http.StatusBadRequest, "INVALID_REQUEST", "Invalid user ID")
			return
		}

		var req updateUserRequest
		if !decodeJSON(w, r, &req) {
			return
		}

		// Load existing user.
		existing, err := a.Queries.GetUser(r.Context(), userID)
		if err != nil {
			a.Logger.Error("get user for update", "error", err)
			writeError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
			return
		}

		name := existing.Name
		if req.Name != nil {
			trimmed := strings.TrimSpace(*req.Name)
			if trimmed == "" {
				writeError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Name must not be empty")
				return
			}
			name = trimmed
		}

		email := existing.Email
		if req.Email != nil {
			if *req.Email == "" {
				email = pgtype.Text{}
			} else {
				if _, err := mail.ParseAddress(*req.Email); err != nil {
					writeError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Invalid email format")
					return
				}
				email = pgconv.Text(*req.Email)
			}
		}

		user, err := a.Queries.UpdateUser(r.Context(), db.UpdateUserParams{
			ID:    userID,
			Name:  name,
			Email: email,
		})
		if err != nil {
			var pgErr *pgconn.PgError
			if errors.As(err, &pgErr) && pgErr.Code == "23505" {
				writeError(w, http.StatusConflict, "DUPLICATE_EMAIL", "A family member with this email already exists")
				return
			}
			a.Logger.Error("update user", "error", err)
			writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to update user")
			return
		}

		if !applyPendingAvatarSeed(a, w, r, userID, req.AvatarSeed) {
			return
		}

		writeJSON(w, http.StatusOK, map[string]any{
			"id":         pgconv.FormatUUID(user.ID),
			"name":       user.Name,
			"email":      pgconv.TextPtr(user.Email),
			"created_at": user.CreatedAt.Time,
			"updated_at": user.UpdatedAt.Time,
		})
	}
}

// applyPendingAvatarSeed persists a Regenerate-button seed staged by
// the avatarEditor Alpine factory. The form submits the same shape
// the regenerate POST used to (clear uploaded image + set seed); we
// just defer that work from the button click to the Save Changes
// click. A nil/empty seed is a no-op. Invalid seeds (too long, bad
// charset) are rejected to bound the upstream DiceBear URL + the
// in-process cache key. Returns false after writing an error
// response.
func applyPendingAvatarSeed(a *app.App, w http.ResponseWriter, r *http.Request, userID pgtype.UUID, seed *string) bool {
	if seed == nil {
		return true
	}
	trimmed := strings.TrimSpace(*seed)
	if trimmed == "" {
		return true
	}
	if !avatar.IsValidSeed(trimmed) {
		writeError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR",
			"Invalid avatar seed — must be 1-128 chars of letters, digits, dot, dash, or underscore")
		return false
	}
	if err := a.Queries.ClearUserAvatar(r.Context(), userID); err != nil {
		a.Logger.Error("clear avatar for deferred regenerate", "error", err)
		writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to update avatar")
		return false
	}
	if err := a.Queries.SetUserAvatarSeed(r.Context(), db.SetUserAvatarSeedParams{
		ID:         userID,
		AvatarSeed: pgconv.Text(trimmed),
	}); err != nil {
		a.Logger.Error("set avatar seed for deferred regenerate", "error", err)
		writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to update avatar")
		return false
	}
	return true
}

// CreateLoginPageHandler serves GET /users/{id}/create-login -- form to create login account.
func CreateLoginPageHandler(a *app.App, tr *TemplateRenderer) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		userID, ok := parseURLUUIDOrNotFound(w, r, tr, "id")
		if !ok {
			return
		}
		idStr := chi.URLParam(r, "id")

		user, err := a.Queries.GetUser(ctx, userID)
		if err != nil {
			tr.RenderNotFound(w, r)
			return
		}

		userEmail := ""
		if user.Email.Valid {
			userEmail = user.Email.String
		}

		// Check if user already has a login account — render manage view.
		loginAccount, err := a.Queries.GetAuthAccountByUserID(ctx, userID)
		if err == nil {
			setupURL := ""
			if loginAccount.SetupToken.Valid {
				scheme := "https"
				if r.TLS == nil {
					scheme = "http"
				}
				setupURL = scheme + "://" + r.Host + "/setup-account/" + loginAccount.SetupToken.String
			}
			breadcrumbs := []Breadcrumb{
				{Label: "Household", Href: "/household"},
				{Label: user.Name, Href: "/household/" + idStr + "/edit"},
				{Label: "Login Account"},
			}
			data := map[string]any{
				"PageTitle":    "Manage Login — " + user.Name,
				"CurrentPage":  "household",
				"IsManage":     true,
				"User":         user,
				"UserID":       idStr,
				"LoginAccount": loginAccount,
				"SetupURL":     setupURL,
				"CSRFToken":    GetCSRFToken(r),
				"Breadcrumbs":  breadcrumbs,
			}
			renderCreateLogin(w, r, tr, data, pages.CreateLoginProps{
				IsManage:         true,
				UserID:           idStr,
				UserName:         user.Name,
				UserEmail:        userEmail,
				LoginAccountID:   pgconv.FormatUUID(loginAccount.ID),
				LoginUsername:    loginAccount.Username,
				LoginRole:        loginAccount.Role,
				LoginPasswordSet: len(loginAccount.HashedPassword) > 0,
				SetupURL:         setupURL,
			})
			return
		}

		// No login account — show create form.
		breadcrumbs := []Breadcrumb{
			{Label: "Household", Href: "/household"},
			{Label: user.Name, Href: "/household/" + idStr + "/edit"},
			{Label: "Create Login"},
		}
		data := map[string]any{
			"PageTitle":   "Create Login — " + user.Name,
			"CurrentPage": "household",
			"IsManage":    false,
			"User":        user,
			"UserID":      idStr,
			"CSRFToken":   GetCSRFToken(r),
			"Breadcrumbs": breadcrumbs,
		}
		renderCreateLogin(w, r, tr, data, pages.CreateLoginProps{
			IsManage:    false,
			UserID:      idStr,
			UserName:    user.Name,
			UserEmail:   userEmail,
		})
	}
}

// renderCreateLogin hosts the typed CreateLogin templ component as a
// sub-view of the top-level Household page.
func renderCreateLogin(w http.ResponseWriter, r *http.Request, tr *TemplateRenderer, data map[string]any, props pages.CreateLoginProps) {
	tr.RenderWithTempl(w, r, data, pages.CreateLogin(props))
}

