package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/dbledeez/panel/controller/internal/ca"
	"github.com/dbledeez/panel/controller/internal/db"
)

// pairService hosts the HTTP endpoints agents use to bootstrap mTLS.
// Two public (no session auth) + one session-auth'd admin endpoint:
//
//	GET  /api/pair/ca         public — returns the CA cert PEM so the
//	                          agent can pin it before completing pair
//	POST /api/pair/issue      public — exchanges {token, agent_id, csr}
//	                          for a signed client cert
//	POST /api/admin/pair-tokens   auth'd — operator creates a new token
type pairService struct {
	ca *ca.CA
	db *db.DB
}

func (p *pairService) getCA(w http.ResponseWriter, _ *http.Request) {
	if p.ca == nil {
		writeError(w, http.StatusServiceUnavailable, "no_ca", "controller has no CA configured (run with --ca-dir)")
		return
	}
	w.Header().Set("Content-Type", "application/x-pem-file")
	_, _ = w.Write(p.ca.RootPEM)
}

type issueReq struct {
	Token   string `json:"token"`
	AgentID string `json:"agent_id"`
	CSRPEM  string `json:"csr_pem"`
}
type issueResp struct {
	CertPEM string `json:"cert_pem"`
	CAPEM   string `json:"ca_pem"`
	Subject string `json:"subject"`
}

func (p *pairService) issue(w http.ResponseWriter, r *http.Request) {
	if p.ca == nil {
		writeError(w, http.StatusServiceUnavailable, "no_ca", "controller has no CA configured")
		return
	}
	var req issueReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if req.Token == "" || req.AgentID == "" || req.CSRPEM == "" {
		writeError(w, http.StatusBadRequest, "missing", "token, agent_id, and csr_pem are required")
		return
	}
	hash := sha256Hex(req.Token)

	// Pre-validate the CSR before consuming the token so a bad CSR
	// doesn't burn the one-time-use token.
	cert, err := p.ca.SignAgentCSR([]byte(req.CSRPEM), req.AgentID)
	if err != nil {
		writeError(w, http.StatusBadRequest, "csr", err.Error())
		return
	}
	fingerprint := certFingerprint(cert)

	if _, err := p.db.ConsumePairToken(r.Context(), hash, req.AgentID, fingerprint); err != nil {
		if errors.Is(err, db.ErrPairTokenNotFound) {
			writeError(w, http.StatusUnauthorized, "invalid_token", "token invalid, expired, or already used")
			return
		}
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	writeJSON(w, http.StatusOK, issueResp{
		CertPEM: string(cert),
		CAPEM:   string(p.ca.RootPEM),
		Subject: req.AgentID,
	})
}

type createTokenReq struct {
	Description    string `json:"description,omitempty"`
	ExpiresMinutes int    `json:"expires_minutes,omitempty"` // default 15
}
type createTokenResp struct {
	ID          string `json:"id"`
	Token       string `json:"token"`       // shown once
	ExpiresAt   string `json:"expires_at"`
	Description string `json:"description"`
}

// createToken: operator-invoked (auth required), generates a raw token
// + stores hash. Returns the raw token exactly once — operator must copy
// it immediately. Token string is never shown or recovered again.
func (p *pairService) createToken(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(sessionUserKey{}).(*db.UserRow)
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthenticated", "login required")
		return
	}
	if user.Role != "admin" {
		writeError(w, http.StatusForbidden, "forbidden", "admin role required")
		return
	}
	var req createTokenReq
	_ = json.NewDecoder(r.Body).Decode(&req)

	ttl := time.Duration(req.ExpiresMinutes) * time.Minute
	if ttl <= 0 {
		ttl = 15 * time.Minute
	}
	raw, err := randomToken(24)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "rand", err.Error())
		return
	}
	id := "pt_" + randHex(8)
	if err := p.db.CreatePairToken(r.Context(), db.PairTokenRow{
		ID:          id,
		TokenHash:   sha256Hex(raw),
		Description: req.Description,
		CreatedBy:   user.ID,
		ExpiresAt:   time.Now().Add(ttl),
	}); err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	writeJSON(w, http.StatusCreated, createTokenResp{
		ID:          id,
		Token:       raw,
		ExpiresAt:   time.Now().Add(ttl).Format(time.RFC3339),
		Description: req.Description,
	})
}

// ---- helpers ----

func sha256Hex(s string) string {
	h := sha256.Sum256([]byte(s))
	return hex.EncodeToString(h[:])
}

func randomToken(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	// URL-safe, no padding, easy to copy/paste.
	return hex.EncodeToString(b), nil
}

func randHex(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

// certFingerprint returns a sha256 hex digest of the DER-encoded cert,
// suitable for identifying which cert was issued against which token.
func certFingerprint(certPEM []byte) string {
	// Cheap approximation — hash the PEM bytes. Operators just need
	// a stable value to link a token to a cert; not a security property.
	return sha256Hex(string(certPEM))
}

// listPairTokens is operator-visible audit of generated tokens.
func (p *pairService) listTokens(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(sessionUserKey{}).(*db.UserRow)
	if !ok || user.Role != "admin" {
		writeError(w, http.StatusForbidden, "forbidden", "admin role required")
		return
	}
	rows, err := p.db.ListPairTokens(r.Context())
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	type dto struct {
		ID          string `json:"id"`
		Description string `json:"description"`
		CreatedAt   string `json:"created_at"`
		ExpiresAt   string `json:"expires_at"`
		UsedAt      string `json:"used_at,omitempty"`
		UsedByAgent string `json:"used_by_agent,omitempty"`
	}
	out := make([]dto, 0, len(rows))
	for _, r := range rows {
		d := dto{
			ID: r.ID, Description: r.Description,
			CreatedAt: r.CreatedAt.Format(time.RFC3339),
			ExpiresAt: r.ExpiresAt.Format(time.RFC3339),
			UsedByAgent: r.UsedByAgent,
		}
		if r.UsedAt != nil {
			d.UsedAt = r.UsedAt.Format(time.RFC3339)
		}
		out = append(out, d)
	}
	writeJSON(w, http.StatusOK, map[string]any{"pair_tokens": out})
}

// Ensure we reference fmt to keep imports stable for future error fmtting.
var _ = fmt.Sprintf
