package appsec

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/niels/banking-app/backend/internal/audit"
	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/httpapi/middleware"
	"github.com/niels/banking-app/backend/internal/platform/httputil"
	"github.com/niels/banking-app/backend/internal/respond"
)

type Handler struct {
	repo  *Repository
	audit *audit.Repository
	log   *slog.Logger
}

type controlRequest struct {
	ControlType       string          `json:"control_type"`
	Status            string          `json:"status"`
	Environment       string          `json:"environment"`
	Owner             string          `json:"owner"`
	Provider          string          `json:"provider"`
	PolicyReference   string          `json:"policy_reference"`
	EvidenceReference string          `json:"evidence_reference"`
	LastVerifiedAt    string          `json:"last_verified_at"`
	NextReviewAt      string          `json:"next_review_at"`
	Metadata          json.RawMessage `json:"metadata"`
}

type keyRotationRequest struct {
	SecretName        string `json:"secret_name"`
	SecretCategory    string `json:"secret_category"`
	Status            string `json:"status"`
	OldKeyReference   string `json:"old_key_reference"`
	NewKeyReference   string `json:"new_key_reference"`
	RotationReason    string `json:"rotation_reason"`
	ScheduledFor      string `json:"scheduled_for"`
	CompletedAt       string `json:"completed_at"`
	EvidenceReference string `json:"evidence_reference"`
}

type keyRotationUpdateRequest struct {
	Status            string `json:"status"`
	NewKeyReference   string `json:"new_key_reference"`
	CompletedAt       string `json:"completed_at"`
	EvidenceReference string `json:"evidence_reference"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, log *slog.Logger) *Handler {
	return &Handler{repo: repo, audit: auditRepo, log: log}
}

func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
	dashboard, err := h.repo.Dashboard(r.Context(), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, dashboard)
}

func (h *Handler) Controls(w http.ResponseWriter, r *http.Request) {
	controls, err := h.repo.ListControls(
		r.Context(),
		strings.TrimSpace(r.URL.Query().Get("type")),
		strings.TrimSpace(r.URL.Query().Get("status")),
		strings.TrimSpace(r.URL.Query().Get("environment")),
		queryLimit(r),
	)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"application_security_controls": controls})
}

func (h *Handler) UpsertControl(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req controlRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	lastVerifiedAt, err := parseOptionalTime(req.LastVerifiedAt, "last_verified_at")
	if err != nil {
		respond.Error(w, err)
		return
	}
	nextReviewAt, err := parseOptionalTime(req.NextReviewAt, "next_review_at")
	if err != nil {
		respond.Error(w, err)
		return
	}
	control, err := h.repo.UpsertControl(r.Context(), ControlParams{
		ControlType:       req.ControlType,
		Status:            req.Status,
		Environment:       req.Environment,
		Owner:             req.Owner,
		Provider:          req.Provider,
		PolicyReference:   req.PolicyReference,
		EvidenceReference: req.EvidenceReference,
		LastVerifiedAt:    lastVerifiedAt,
		NextReviewAt:      nextReviewAt,
		Metadata:          req.Metadata,
		AdminUserID:       claims.Subject,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, claims.Subject, "admin.application_security_control.upserted", "application_security_control", control.ID, map[string]any{
		"control_type":       control.ControlType,
		"status":             control.Status,
		"environment":        control.Environment,
		"evidence_reference": control.EvidenceReference,
	})
	respond.JSON(w, http.StatusOK, control)
}

func (h *Handler) KeyRotationRuns(w http.ResponseWriter, r *http.Request) {
	runs, err := h.repo.ListKeyRotationRuns(
		r.Context(),
		strings.TrimSpace(r.URL.Query().Get("status")),
		queryLimit(r),
	)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"key_rotation_runs": runs})
}

func (h *Handler) CreateKeyRotationRun(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req keyRotationRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	scheduledFor, err := parseRequiredTime(req.ScheduledFor, "scheduled_for")
	if err != nil {
		respond.Error(w, err)
		return
	}
	completedAt, err := parseOptionalTime(req.CompletedAt, "completed_at")
	if err != nil {
		respond.Error(w, err)
		return
	}
	run, err := h.repo.CreateKeyRotationRun(r.Context(), RotationParams{
		SecretName:        req.SecretName,
		SecretCategory:    req.SecretCategory,
		Status:            req.Status,
		OldKeyReference:   req.OldKeyReference,
		NewKeyReference:   req.NewKeyReference,
		RotationReason:    req.RotationReason,
		ScheduledFor:      scheduledFor,
		CompletedAt:       completedAt,
		EvidenceReference: req.EvidenceReference,
		AdminUserID:       claims.Subject,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, claims.Subject, "admin.key_rotation_run.created", "key_rotation_run", run.ID, map[string]any{
		"secret_name":     run.SecretName,
		"secret_category": run.SecretCategory,
		"status":          run.Status,
		"scheduled_for":   run.ScheduledFor,
	})
	respond.Created(w, run)
}

func (h *Handler) UpdateKeyRotationRun(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req keyRotationUpdateRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	completedAt, err := parseOptionalTime(req.CompletedAt, "completed_at")
	if err != nil {
		respond.Error(w, err)
		return
	}
	run, err := h.repo.UpdateKeyRotationRun(r.Context(), RotationUpdateParams{
		ID:                r.PathValue("id"),
		Status:            req.Status,
		NewKeyReference:   req.NewKeyReference,
		CompletedAt:       completedAt,
		EvidenceReference: req.EvidenceReference,
		AdminUserID:       claims.Subject,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, claims.Subject, "admin.key_rotation_run.updated", "key_rotation_run", run.ID, map[string]any{
		"secret_name":        run.SecretName,
		"secret_category":    run.SecretCategory,
		"status":             run.Status,
		"evidence_reference": run.EvidenceReference,
	})
	respond.JSON(w, http.StatusOK, run)
}

func parseRequiredTime(raw, field string) (time.Time, error) {
	parsed, err := parseOptionalTime(raw, field)
	if err != nil {
		return time.Time{}, err
	}
	if parsed == nil {
		return time.Time{}, fmt.Errorf("%w: %s is required", domain.ErrValidation, field)
	}
	return *parsed, nil
}

func parseOptionalTime(raw, field string) (*time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	parsed, err := time.Parse(time.RFC3339, raw)
	if err != nil {
		return nil, fmt.Errorf("%w: %s must be RFC3339", domain.ErrValidation, field)
	}
	utc := parsed.UTC()
	return &utc, nil
}

func (h *Handler) recordAudit(r *http.Request, adminUserID, eventType, targetType, targetID string, metadata any) {
	if h.audit == nil {
		return
	}
	if err := h.audit.Record(r.Context(), audit.Event{
		ActorUserID: &adminUserID,
		EventType:   eventType,
		TargetType:  targetType,
		TargetID:    targetID,
		Metadata:    metadata,
		RemoteIP:    httputil.RemoteIP(r),
		UserAgent:   r.UserAgent(),
	}); err != nil && h.log != nil {
		h.log.Error("audit record failed", "error", err)
	}
}

func queryLimit(r *http.Request) int {
	limit := 50
	if raw := r.URL.Query().Get("limit"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			limit = parsed
		}
	}
	return limit
}
