package api

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"

	"archivmail/internal/storage"
)

// PROJ-43: Tenant routing rules CRUD + dry-run.
//
// Scope model (see PROJ-55/61/62/63 security fixes):
//   - Superadmin (sess.TenantID == nil): may see/manage rules for ALL tenants.
//   - Domain admin (sess.TenantID set): may only see/manage rules whose
//     tenant_id matches their own tenant. Every {id} path additionally verifies
//     ownership via tenantAccessAllowed() to prevent IDOR.

type routingRuleBody struct {
	TenantID  *int64 `json:"tenant_id"`
	MatchType string `json:"match_type"`
	Pattern   string `json:"pattern"`
	Priority  int    `json:"priority"`
}

// resolveRuleTenant determines the tenant_id a rule must belong to for the
// current session, and reports whether the request is allowed.
//   - Superadmin: must specify tenant_id in the body (rules always target a
//     concrete tenant); any tenant allowed.
//   - Domain admin: tenant_id is forced to their own tenant; a mismatching
//     explicit body value is rejected.
func (s *Server) resolveRuleTenant(sess sessionTenant, bodyTenantID *int64) (int64, bool) {
	if sess.tenantID == nil {
		// superadmin
		if bodyTenantID == nil || *bodyTenantID <= 0 {
			return 0, false
		}
		return *bodyTenantID, true
	}
	if bodyTenantID != nil && *bodyTenantID != *sess.tenantID {
		return 0, false
	}
	return *sess.tenantID, true
}

// sessionTenant is a tiny helper capturing what the handlers need from a session.
type sessionTenant struct {
	tenantID *int64
}

func sessTenant(r *http.Request) sessionTenant {
	sess := sessionFromCtx(r.Context())
	return sessionTenant{tenantID: sess.TenantID}
}

// handleListRoutingRules returns routing rules visible to the caller.
// GET /api/admin/routing-rules
func (s *Server) handleListRoutingRules(w http.ResponseWriter, r *http.Request) {
	scope := sessTenant(r).tenantID // nil for superadmin → all rules
	rules, err := s.store.ListTenantRoutingRules(r.Context(), scope)
	if err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}
	if rules == nil {
		rules = []storage.TenantRoutingRule{}
	}
	writeJSON(w, http.StatusOK, map[string]interface{}{"rules": rules})
}

// handleCreateRoutingRule creates a new routing rule.
// POST /api/admin/routing-rules
func (s *Server) handleCreateRoutingRule(w http.ResponseWriter, r *http.Request) {
	var body routingRuleBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid body")
		return
	}
	tenantID, ok := s.resolveRuleTenant(sessTenant(r), body.TenantID)
	if !ok {
		writeError(w, http.StatusForbidden, "tenant_id required and must match your scope")
		return
	}
	id, err := s.store.CreateTenantRoutingRule(r.Context(), storage.TenantRoutingRule{
		TenantID:  tenantID,
		MatchType: body.MatchType,
		Pattern:   body.Pattern,
		Priority:  body.Priority,
	})
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	s.auditRule(r, "routing_rule_created", fmt.Sprintf("id=%d tenant=%d type=%s pattern=%s prio=%d",
		id, tenantID, body.MatchType, body.Pattern, body.Priority))
	writeJSON(w, http.StatusCreated, map[string]interface{}{"id": id})
}

// handleUpdateRoutingRule updates an existing routing rule.
// PUT /api/admin/routing-rules/{id}
func (s *Server) handleUpdateRoutingRule(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid rule id")
		return
	}
	// IDOR: load existing rule and verify ownership before mutating.
	existing, err := s.store.GetTenantRoutingRule(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusNotFound, "rule not found")
		return
	}
	sess := sessionFromCtx(r.Context())
	if !tenantAccessAllowed(sess, &existing.TenantID) {
		writeError(w, http.StatusForbidden, "forbidden")
		return
	}
	var body routingRuleBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid body")
		return
	}
	tenantID, ok := s.resolveRuleTenant(sessTenant(r), body.TenantID)
	if !ok {
		writeError(w, http.StatusForbidden, "tenant_id must match your scope")
		return
	}
	if err := s.store.UpdateTenantRoutingRule(r.Context(), storage.TenantRoutingRule{
		ID:        id,
		TenantID:  tenantID,
		MatchType: body.MatchType,
		Pattern:   body.Pattern,
		Priority:  body.Priority,
	}); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	s.auditRule(r, "routing_rule_updated", fmt.Sprintf("id=%d tenant=%d type=%s pattern=%s prio=%d",
		id, tenantID, body.MatchType, body.Pattern, body.Priority))
	writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}

// handleDeleteRoutingRule deletes a routing rule.
// DELETE /api/admin/routing-rules/{id}
func (s *Server) handleDeleteRoutingRule(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid rule id")
		return
	}
	existing, err := s.store.GetTenantRoutingRule(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusNotFound, "rule not found")
		return
	}
	sess := sessionFromCtx(r.Context())
	if !tenantAccessAllowed(sess, &existing.TenantID) {
		writeError(w, http.StatusForbidden, "forbidden")
		return
	}
	if err := s.store.DeleteTenantRoutingRule(r.Context(), id); err != nil {
		writeError(w, http.StatusNotFound, err.Error())
		return
	}
	s.auditRule(r, "routing_rule_deleted", fmt.Sprintf("id=%d", id))
	writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}

type routingDryRunBody struct {
	RuleID    *int64 `json:"rule_id"`    // dry-run an existing rule, OR ...
	MatchType string `json:"match_type"` // ... an ad-hoc (match_type, pattern)
	Pattern   string `json:"pattern"`
	Limit     int    `json:"limit"`
}

// handleDryRunRoutingRule previews which already-archived mails a rule would
// match. Bounded by LIMIT to avoid full-scan timeouts on large archives.
// POST /api/admin/routing-rules/dry-run
func (s *Server) handleDryRunRoutingRule(w http.ResponseWriter, r *http.Request) {
	var body routingDryRunBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid body")
		return
	}
	sess := sessionFromCtx(r.Context())

	matchType, pattern := body.MatchType, body.Pattern
	if body.RuleID != nil {
		rule, err := s.store.GetTenantRoutingRule(r.Context(), *body.RuleID)
		if err != nil {
			writeError(w, http.StatusNotFound, "rule not found")
			return
		}
		if !tenantAccessAllowed(sess, &rule.TenantID) {
			writeError(w, http.StatusForbidden, "forbidden")
			return
		}
		matchType, pattern = rule.MatchType, rule.Pattern
	}

	// Domain admins may only preview mails within their own tenant.
	scope := sess.TenantID
	res, err := s.store.DryRunRoutingRule(r.Context(), matchType, pattern, body.Limit, scope)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	s.auditRule(r, "routing_rule_dry_run", fmt.Sprintf("type=%s pattern=%s matches=%d", matchType, pattern, res.MatchCount))
	writeJSON(w, http.StatusOK, res)
}

// NOTE: auditRule is defined in archiving_rules_handlers.go and reused here.
