package admin

import (
	"context"
	"encoding/csv"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// Handlers holds the dependencies for all admin HTTP handlers.
type Handlers struct {
	cfg      *Config
	store    Store
	sessions *SessionStore
	auth     *Authenticator
	svc      Services
	sec      *SecurityLog
	render   *renderer
	logger   *log.Logger
}

// NewHandlers constructs the handler set.
func NewHandlers(cfg *Config, store Store, sessions *SessionStore, auth *Authenticator,
	svc Services, sec *SecurityLog, logger *log.Logger) (*Handlers, error) {
	r, err := newRenderer()
	if err != nil {
		return nil, err
	}
	if logger == nil {
		logger = log.Default()
	}
	return &Handlers{
		cfg: cfg, store: store, sessions: sessions, auth: auth,
		svc: svc, sec: sec, render: r, logger: logger,
	}, nil
}

const batchListLimit = 50
const auditListLimit = 50

// --------------------------------------------------------------------------
// Auth
// --------------------------------------------------------------------------

// LoginPage renders the login form (GET /login).
func (h *Handlers) LoginPage(w http.ResponseWriter, r *http.Request) {
	h.render.render(w, "login", pageData{Flash: r.URL.Query().Get("e")})
}

// LoginSubmit processes the login form (POST /login).
func (h *Handlers) LoginSubmit(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		h.render.render(w, "login", pageData{Flash: "请求无效"})
		return
	}
	username := strings.TrimSpace(r.PostFormValue("username"))
	password := r.PostFormValue("password")
	code := strings.TrimSpace(r.PostFormValue("totp"))
	ip := hostOnly(r.RemoteAddr)

	sid, _, err := h.auth.Login(r.Context(), username, password, code, ip)
	if err != nil {
		flash := "用户名、密码或动态验证码有误"
		if err == ErrLockedOut {
			flash = "尝试过于频繁，账户已临时锁定，请稍后再试"
		}
		w.WriteHeader(http.StatusUnauthorized)
		h.render.render(w, "login", pageData{Flash: flash})
		return
	}
	h.setSessionCookie(w, sid)
	http.Redirect(w, r, "/", http.StatusFound)
}

// Logout destroys the session (POST /logout).
func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	if sess == nil || !h.validCSRF(r, sess) {
		http.Error(w, "invalid csrf", http.StatusForbidden)
		return
	}
	if c, err := r.Cookie(SessionCookieName); err == nil {
		_ = h.sessions.Delete(r.Context(), c.Value)
	}
	h.clearSessionCookie(w)
	http.Redirect(w, r, "/login", http.StatusFound)
}

// --------------------------------------------------------------------------
// Dashboard
// --------------------------------------------------------------------------

// Dashboard renders the landing page (GET /).
func (h *Handlers) Dashboard(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	h.render.render(w, "dashboard", pageData{
		Username: sess.Username, CSRF: sess.CSRFToken, Active: "dashboard",
	})
}

// --------------------------------------------------------------------------
// Code batches
// --------------------------------------------------------------------------

type codesView struct {
	Batches    []BatchSummary
	HasPrev    bool
	HasNext    bool
	PrevOffset int
	NextOffset int
}

// CodesPage lists batches and shows the generate form (GET /codes).
func (h *Handlers) CodesPage(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
	if offset < 0 {
		offset = 0
	}
	batches, total, err := h.svc.Codes.ListBatches(r.Context(), batchListLimit, offset)
	if err != nil {
		h.serverError(w, "list batches", err)
		return
	}
	view := codesView{
		Batches:    batches,
		HasPrev:    offset > 0,
		PrevOffset: maxInt(0, offset-batchListLimit),
		HasNext:    offset+batchListLimit < total,
		NextOffset: offset + batchListLimit,
	}
	h.render.render(w, "codes", pageData{
		Username: sess.Username, CSRF: sess.CSRFToken, Active: "codes",
		Flash: r.URL.Query().Get("flash"), Data: view,
	})
}

// CreateBatch generates a batch and streams the plaintext CSV (POST /codes).
// The plaintext codes appear ONLY in this response body — never persisted or
// logged.
func (h *Handlers) CreateBatch(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
	if !h.validCSRF(r, sess) {
		http.Error(w, "invalid csrf", http.StatusForbidden)
		return
	}

	plan := strings.TrimSpace(r.PostFormValue("plan"))
	channel := strings.TrimSpace(r.PostFormValue("channel"))
	note := strings.TrimSpace(r.PostFormValue("note"))
	duration, derr := strconv.Atoi(r.PostFormValue("duration_days"))
	count, cerr := strconv.Atoi(r.PostFormValue("count"))
	if derr != nil || cerr != nil || duration < 1 || duration > 3650 || count < 1 || count > 5000 {
		http.Error(w, "参数无效：时长 1-3650 天，数量 1-5000", http.StatusBadRequest)
		return
	}

	batch, err := h.svc.Codes.CreateBatch(r.Context(), CodeBatchParams{
		Plan: plan, DurationDays: duration, Count: count, Channel: channel,
		Note: note, CreatedBy: "admin:" + sess.Username,
	})
	if err != nil {
		h.serverError(w, "create batch", err)
		return
	}

	// Audit: record metadata only (NEVER the plaintext codes).
	h.writeAudit(r.Context(), sess.Username, "code_batch_create",
		fmt.Sprintf("batch:%d", batch.BatchID),
		fmt.Sprintf(`{"plan":%q,"duration_days":%d,"count":%d,"channel":%q}`,
			batch.Plan, batch.DurationDays, len(batch.Codes), batch.Channel))

	h.streamBatchCSV(w, batch)
}

// streamBatchCSV writes the one-time plaintext CSV download.
func (h *Handlers) streamBatchCSV(w http.ResponseWriter, batch *GeneratedBatch) {
	filename := fmt.Sprintf("batch-%d-%s.csv", batch.BatchID, batch.GeneratedAt.Format("20060102T150405Z"))
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
	w.Header().Set("Cache-Control", "no-store")
	cw := csv.NewWriter(w)
	_ = cw.Write([]string{"index", "code", "plan", "duration_days", "batch_id", "channel"})
	for i, code := range batch.Codes {
		_ = cw.Write([]string{
			strconv.Itoa(i + 1), code, batch.Plan,
			strconv.Itoa(batch.DurationDays), strconv.FormatInt(batch.BatchID, 10), batch.Channel,
		})
	}
	cw.Flush()
}

// VoidBatch voids all unused codes in a batch (POST /codes/void).
func (h *Handlers) VoidBatch(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	if !h.checkWrite(w, r, sess) {
		return
	}
	batchID, err := strconv.ParseInt(r.PostFormValue("batch_id"), 10, 64)
	if err != nil || batchID <= 0 {
		http.Error(w, "bad batch_id", http.StatusBadRequest)
		return
	}
	affected, verr := h.svc.Codes.VoidBatch(r.Context(), batchID)
	if verr != nil {
		h.serverError(w, "void batch", verr)
		return
	}
	h.writeAudit(r.Context(), sess.Username, "code_batch_void",
		fmt.Sprintf("batch:%d", batchID),
		fmt.Sprintf(`{"voided":%d}`, affected))
	h.redirectFlash(w, r, "/codes", fmt.Sprintf("已作废批次 %d 的 %d 个未使用激活码", batchID, affected))
}

// --------------------------------------------------------------------------
// Nodes
// --------------------------------------------------------------------------

type nodesView struct {
	Nodes          []NodeRow
	ProvisionReady bool
	LifecycleReady bool
}

// NodesPage lists nodes and operation controls (GET /nodes).
func (h *Handlers) NodesPage(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	nodes, err := h.store.ListNodes(r.Context(), 3)
	if err != nil {
		h.serverError(w, "list nodes", err)
		return
	}
	h.render.render(w, "nodes", pageData{
		Username: sess.Username, CSRF: sess.CSRFToken, Active: "nodes",
		Flash: r.URL.Query().Get("flash"),
		Data: nodesView{
			Nodes:          nodes,
			ProvisionReady: h.svc.Provision.Ready(),
			LifecycleReady: h.svc.Lifecycle.Ready(),
		},
	})
}

// NodeOp dispatches replace / draining / up (POST /nodes/op).
func (h *Handlers) NodeOp(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	if !h.checkWrite(w, r, sess) {
		return
	}
	nodeID, err := strconv.ParseInt(r.PostFormValue("node_id"), 10, 64)
	if err != nil || nodeID <= 0 {
		http.Error(w, "bad node_id", http.StatusBadRequest)
		return
	}
	op := r.PostFormValue("op")

	var opErr error
	var action string
	switch op {
	case "replace":
		action = "node_replace"
		opErr = h.svc.Provision.Replace(r.Context(), nodeID, sess.Username)
	case "draining", "up":
		action = "node_" + op
		opErr = h.svc.Lifecycle.TransitionStatus(r.Context(), nodeID, op, sess.Username)
	default:
		http.Error(w, "unknown op", http.StatusBadRequest)
		return
	}

	if opErr != nil {
		if opErr == ErrServiceUnavailable {
			h.redirectFlash(w, r, "/nodes", "该操作所依赖的服务尚未接入")
			return
		}
		h.serverError(w, "node op", opErr)
		return
	}

	h.writeAudit(r.Context(), sess.Username, action,
		fmt.Sprintf("node:%d", nodeID), fmt.Sprintf(`{"op":%q}`, op))
	h.redirectFlash(w, r, "/nodes", fmt.Sprintf("节点 %d 操作 %s 已执行", nodeID, op))
}

// --------------------------------------------------------------------------
// Audit
// --------------------------------------------------------------------------

type auditView struct {
	Filter     AuditFilter
	FromStr    string
	ToStr      string
	Entries    []AuditEntry
	Total      int
	HasPrev    bool
	HasNext    bool
	PrevURL    string
	NextURL    string
	NodeEvents []NodeEvent
	NodeIDStr  string
}

// AuditPage renders the filtered audit log and optional node events (GET /audit).
func (h *Handlers) AuditPage(w http.ResponseWriter, r *http.Request) {
	sess := SessionFromContext(r.Context())
	q := r.URL.Query()

	offset, _ := strconv.Atoi(q.Get("offset"))
	if offset < 0 {
		offset = 0
	}
	fromStr := strings.TrimSpace(q.Get("from"))
	toStr := strings.TrimSpace(q.Get("to"))

	f := AuditFilter{
		Actor:  strings.TrimSpace(q.Get("actor")),
		Action: strings.TrimSpace(q.Get("action")),
		Target: strings.TrimSpace(q.Get("target")),
		From:   parseDate(fromStr, false),
		To:     parseDate(toStr, true),
		Limit:  auditListLimit,
		Offset: offset,
	}

	entries, total, err := h.store.QueryAudit(r.Context(), f)
	if err != nil {
		h.serverError(w, "query audit", err)
		return
	}

	var nodeEvents []NodeEvent
	nodeIDStr := strings.TrimSpace(q.Get("node_id"))
	if nodeIDStr != "" {
		if nid, perr := strconv.ParseInt(nodeIDStr, 10, 64); perr == nil && nid > 0 {
			nodeEvents, err = h.store.QueryNodeEvents(r.Context(), nid, 50)
			if err != nil {
				h.serverError(w, "query node events", err)
				return
			}
		}
	}

	view := auditView{
		Filter: f, FromStr: fromStr, ToStr: toStr,
		Entries: entries, Total: total,
		HasPrev: offset > 0, HasNext: offset+auditListLimit < total,
		PrevURL:    auditURL(q, maxInt(0, offset-auditListLimit)),
		NextURL:    auditURL(q, offset+auditListLimit),
		NodeEvents: nodeEvents, NodeIDStr: nodeIDStr,
	}
	h.render.render(w, "audit", pageData{
		Username: sess.Username, CSRF: sess.CSRFToken, Active: "audit", Data: view,
	})
}

// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------

func (h *Handlers) validCSRF(r *http.Request, sess *Session) bool {
	return sess != nil && sess.ValidCSRF(r.PostFormValue("csrf_token"))
}

// checkWrite enforces CSRF + explicit second confirmation for write ops.
func (h *Handlers) checkWrite(w http.ResponseWriter, r *http.Request, sess *Session) bool {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return false
	}
	if !h.validCSRF(r, sess) {
		http.Error(w, "invalid csrf", http.StatusForbidden)
		return false
	}
	if r.PostFormValue("confirm") != "yes" {
		http.Error(w, "confirmation required", http.StatusBadRequest)
		return false
	}
	return true
}

func (h *Handlers) writeAudit(ctx context.Context, actor, action, target, metaJSON string) {
	if err := h.store.WriteAudit(ctx, actor, action, target, metaJSON); err != nil {
		h.logger.Printf("admin: audit write failed action=%s target=%s: %v", action, target, err)
	}
}

func (h *Handlers) setSessionCookie(w http.ResponseWriter, sid string) {
	http.SetCookie(w, &http.Cookie{
		Name:     SessionCookieName,
		Value:    sid,
		Path:     "/",
		HttpOnly: true,
		Secure:   h.cfg.CookieSecure,
		SameSite: http.SameSiteStrictMode,
		MaxAge:   int(h.cfg.SessionTTL.Seconds()),
	})
}

func (h *Handlers) clearSessionCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     SessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		Secure:   h.cfg.CookieSecure,
		SameSite: http.SameSiteStrictMode,
		MaxAge:   -1,
	})
}

func (h *Handlers) redirectFlash(w http.ResponseWriter, r *http.Request, path, msg string) {
	http.Redirect(w, r, path+"?flash="+url.QueryEscape(msg), http.StatusFound)
}

func (h *Handlers) serverError(w http.ResponseWriter, what string, err error) {
	h.logger.Printf("admin: %s: %v", what, err)
	http.Error(w, "服务器内部错误", http.StatusInternalServerError)
}

func auditURL(q url.Values, offset int) string {
	nq := url.Values{}
	for _, k := range []string{"actor", "action", "target", "from", "to", "node_id"} {
		if v := q.Get(k); v != "" {
			nq.Set(k, v)
		}
	}
	nq.Set("offset", strconv.Itoa(offset))
	return "/audit?" + nq.Encode()
}

// parseDate accepts "2006-01-02" or RFC3339. When endOfDay is true a bare date
// is pushed to 23:59:59 so the range is inclusive.
func parseDate(s string, endOfDay bool) *time.Time {
	if s == "" {
		return nil
	}
	if t, err := time.ParseInLocation("2006-01-02", s, time.UTC); err == nil {
		if endOfDay {
			t = t.Add(24*time.Hour - time.Second)
		}
		return &t
	}
	if t, err := time.Parse(time.RFC3339, s); err == nil {
		tu := t.UTC()
		return &tu
	}
	return nil
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}
