package storage

import (
	"context"
	"fmt"
	"strings"
	"time"

	"archivmail/pkg/mailparser"
)

// Retention source markers stored in emails.retain_until_source for auditor
// traceability (PROJ-51). The "rule:<id>" form is built dynamically.
const (
	RetainSourceTenantDefault = "tenant_default"
	RetainSourceGlobalDefault = "global_default"
	RetainSourceMinRetention  = "min_retention"
)

// Archiving rule condition types (PROJ-51 / minimal PROJ-43).
const (
	RuleCondSenderDomain    = "sender_domain"
	RuleCondRecipientDomain = "recipient_domain"
	RuleCondSenderAddress   = "sender_address"
	RuleCondRecipientAddr   = "recipient_address"
)

// ArchivingRule is a minimal PROJ-43 rule extended with a PROJ-51 retention
// period. A rule with RetentionDays != nil determines the retention period of
// every mail it matches (highest priority wins, longest on a tie).
type ArchivingRule struct {
	ID            int64     `json:"id"`
	TenantID      *int64    `json:"tenant_id"`      // nil = applies to all tenants
	CondType      string    `json:"condition_type"` // RuleCond*
	Pattern       string    `json:"pattern"`        // e.g. "firma.de" or "rechnung@firma.de"
	Priority      int       `json:"priority"`       // higher = evaluated first
	RetentionDays *int      `json:"retention_days"` // nil = no rule-specific retention
	CreatedAt     time.Time `json:"created_at"`
}

// minRetentionDays is the global floor (config storage.min_retention_days).
// Rules and tenant settings may only extend (never shorten) the period below
// this value (PROJ-51).
func (s *Store) initRetentionRulesSchema(ctx context.Context) {
	if s.db == nil {
		return
	}
	_, _ = s.db.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS archiving_rules (
			id             BIGSERIAL PRIMARY KEY,
			tenant_id      BIGINT REFERENCES tenants(id) ON DELETE CASCADE,
			condition_type TEXT NOT NULL,
			pattern        TEXT NOT NULL,
			priority       INT  NOT NULL DEFAULT 0,
			retention_days INT,
			created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
		)`)
	_, _ = s.db.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_archiving_rules_priority ON archiving_rules (priority DESC)`)
	// PROJ-51: traceability for which source set retain_until.
	_, _ = s.db.Exec(ctx, `ALTER TABLE emails ADD COLUMN IF NOT EXISTS retain_until_source TEXT`)
}

// SetMinRetentionDays wires the global minimum retention floor (PROJ-51).
func (s *Store) SetMinRetentionDays(days int) {
	if days < 0 {
		days = 0
	}
	s.minRetentionDays = days
}

// MinRetentionDays returns the configured global minimum retention floor.
func (s *Store) MinRetentionDays() int { return s.minRetentionDays }

// ── Rule CRUD ────────────────────────────────────────────────────────────────

func validCondType(t string) bool {
	switch t {
	case RuleCondSenderDomain, RuleCondRecipientDomain, RuleCondSenderAddress, RuleCondRecipientAddr:
		return true
	}
	return false
}

// ListArchivingRules returns all rules ordered by priority (desc). If tenantID
// is non-nil, only rules for that tenant plus global rules (tenant_id IS NULL)
// are returned.
func (s *Store) ListArchivingRules(ctx context.Context, tenantID *int64) ([]ArchivingRule, error) {
	if s.db == nil {
		return nil, fmt.Errorf("storage: no db")
	}
	query := `SELECT id, tenant_id, condition_type, pattern, priority, retention_days, created_at
	          FROM archiving_rules`
	var args []interface{}
	if tenantID != nil {
		query += ` WHERE tenant_id = $1 OR tenant_id IS NULL`
		args = append(args, *tenantID)
	}
	query += ` ORDER BY priority DESC, id ASC`

	rows, err := s.db.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("storage: list rules: %w", err)
	}
	defer rows.Close()

	var out []ArchivingRule
	for rows.Next() {
		var r ArchivingRule
		if err := rows.Scan(&r.ID, &r.TenantID, &r.CondType, &r.Pattern, &r.Priority, &r.RetentionDays, &r.CreatedAt); err != nil {
			return nil, fmt.Errorf("storage: scan rule: %w", err)
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// CreateArchivingRule inserts a new rule and returns its generated ID.
func (s *Store) CreateArchivingRule(ctx context.Context, r ArchivingRule) (int64, error) {
	if s.db == nil {
		return 0, fmt.Errorf("storage: no db")
	}
	if !validCondType(r.CondType) {
		return 0, fmt.Errorf("storage: invalid condition_type %q", r.CondType)
	}
	if strings.TrimSpace(r.Pattern) == "" {
		return 0, fmt.Errorf("storage: pattern must not be empty")
	}
	if r.RetentionDays != nil && *r.RetentionDays < 0 {
		return 0, fmt.Errorf("storage: retention_days must be >= 0")
	}
	var id int64
	err := s.db.QueryRow(ctx, `
		INSERT INTO archiving_rules (tenant_id, condition_type, pattern, priority, retention_days)
		VALUES ($1, $2, $3, $4, $5) RETURNING id`,
		r.TenantID, r.CondType, strings.ToLower(strings.TrimSpace(r.Pattern)), r.Priority, r.RetentionDays,
	).Scan(&id)
	if err != nil {
		return 0, fmt.Errorf("storage: create rule: %w", err)
	}
	return id, nil
}

// UpdateArchivingRule replaces all editable fields of an existing rule.
func (s *Store) UpdateArchivingRule(ctx context.Context, r ArchivingRule) error {
	if s.db == nil {
		return fmt.Errorf("storage: no db")
	}
	if !validCondType(r.CondType) {
		return fmt.Errorf("storage: invalid condition_type %q", r.CondType)
	}
	if strings.TrimSpace(r.Pattern) == "" {
		return fmt.Errorf("storage: pattern must not be empty")
	}
	if r.RetentionDays != nil && *r.RetentionDays < 0 {
		return fmt.Errorf("storage: retention_days must be >= 0")
	}
	tag, err := s.db.Exec(ctx, `
		UPDATE archiving_rules
		SET tenant_id=$1, condition_type=$2, pattern=$3, priority=$4, retention_days=$5
		WHERE id=$6`,
		r.TenantID, r.CondType, strings.ToLower(strings.TrimSpace(r.Pattern)), r.Priority, r.RetentionDays, r.ID,
	)
	if err != nil {
		return fmt.Errorf("storage: update rule: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("storage: rule %d not found", r.ID)
	}
	return nil
}

// DeleteArchivingRule removes a rule. Existing retain_until values on already
// archived mails are intentionally left unchanged (PROJ-51 edge case: no
// retroactive shortening).
func (s *Store) DeleteArchivingRule(ctx context.Context, id int64) error {
	if s.db == nil {
		return fmt.Errorf("storage: no db")
	}
	tag, err := s.db.Exec(ctx, `DELETE FROM archiving_rules WHERE id=$1`, id)
	if err != nil {
		return fmt.Errorf("storage: delete rule: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("storage: rule %d not found", id)
	}
	return nil
}

// ── Retention computation ────────────────────────────────────────────────────

// ruleMatches reports whether a parsed mail matches a rule's condition.
func ruleMatches(r ArchivingRule, pm *mailparser.ParsedMail) bool {
	if pm == nil {
		return false
	}
	pat := strings.ToLower(strings.TrimSpace(r.Pattern))
	if pat == "" {
		return false
	}
	domainOf := func(addr string) string {
		addr = strings.ToLower(strings.TrimSpace(addr))
		if i := strings.LastIndex(addr, "@"); i >= 0 {
			return addr[i+1:]
		}
		return ""
	}
	switch r.CondType {
	case RuleCondSenderAddress:
		return strings.ToLower(strings.TrimSpace(pm.From)) == pat
	case RuleCondSenderDomain:
		return domainOf(pm.From) == pat
	case RuleCondRecipientAddr:
		for _, t := range pm.To {
			if strings.ToLower(strings.TrimSpace(t)) == pat {
				return true
			}
		}
		for _, c := range pm.CC {
			if strings.ToLower(strings.TrimSpace(c)) == pat {
				return true
			}
		}
		return false
	case RuleCondRecipientDomain:
		for _, t := range pm.To {
			if domainOf(t) == pat {
				return true
			}
		}
		for _, c := range pm.CC {
			if domainOf(c) == pat {
				return true
			}
		}
		return false
	}
	return false
}

// resolveRetention computes the retention period (in days), the source marker,
// and whether any lock applies for a mail. It implements the PROJ-51 ordering:
//  1. matching rule with retention_days (highest priority, longest on tie)
//  2. tenant_default (tenants.retention_days > 0)
//  3. global_default (s.retentionDays > 0, only when no tenant)
//  4. min_retention floor — may only extend the above
//
// Returns days=0 and apply=false when no lock applies at all.
func (s *Store) resolveRetention(ctx context.Context, pm *mailparser.ParsedMail, tenantID *int64) (days int, source string, apply bool) {
	// Step 1: matching rule
	if s.db != nil && pm != nil {
		rules, err := s.ListArchivingRules(ctx, tenantID)
		if err == nil {
			var best *ArchivingRule
			for i := range rules {
				r := rules[i]
				if r.RetentionDays == nil {
					continue
				}
				// Tenant scoping: a tenant-specific rule only applies to its own
				// tenant; a global rule (nil) applies everywhere.
				if r.TenantID != nil {
					if tenantID == nil || *r.TenantID != *tenantID {
						continue
					}
				}
				if !ruleMatches(r, pm) {
					continue
				}
				if best == nil {
					rr := r
					best = &rr
					continue
				}
				// Highest priority wins; on tie the longest retention wins.
				if r.Priority > best.Priority ||
					(r.Priority == best.Priority && *r.RetentionDays > *best.RetentionDays) {
					rr := r
					best = &rr
				}
			}
			if best != nil {
				days = *best.RetentionDays
				source = fmt.Sprintf("rule:%d", best.ID)
				apply = true
			}
		}
	}

	// Step 2/3: tenant / global default (only if no rule set a value)
	if !apply {
		if tenantID != nil && s.db != nil {
			var tenantDays int
			if err := s.db.QueryRow(ctx, `SELECT retention_days FROM tenants WHERE id=$1`, *tenantID).Scan(&tenantDays); err == nil && tenantDays > 0 {
				days = tenantDays
				source = RetainSourceTenantDefault
				apply = true
			}
		} else if tenantID == nil && s.retentionDays > 0 {
			days = s.retentionDays
			source = RetainSourceGlobalDefault
			apply = true
		}
	}

	// Step 4: global minimum floor — may only extend.
	if s.minRetentionDays > 0 && s.minRetentionDays > days {
		days = s.minRetentionDays
		source = RetainSourceMinRetention
		apply = true
	}

	return days, source, apply
}

// applyRetention computes and writes retain_until + retain_until_source for a
// freshly stored mail (PROJ-51). It never shortens an existing retain_until:
// it only writes when retain_until IS NULL.
func (s *Store) applyRetention(ctx context.Context, id string, pm *mailparser.ParsedMail, tenantID *int64) {
	if s.db == nil {
		return
	}
	days, source, apply := s.resolveRetention(ctx, pm, tenantID)
	if !apply || days <= 0 {
		return
	}
	until := time.Now().AddDate(0, 0, days)
	_, _ = s.db.Exec(ctx,
		`UPDATE emails SET retain_until=$1, retain_until_source=$2 WHERE id=$3 AND retain_until IS NULL`,
		until, source, id)
}

// ApplyRetentionBackfill is the exported entry point for runBackfill (PROJ-34 /
// PROJ-51). It (re-)evaluates rules, tenant/global defaults and the minimum
// floor for an existing mail and sets retain_until only when it is still NULL.
// It never shortens an existing lock.
func (s *Store) ApplyRetentionBackfill(ctx context.Context, id string, pm *mailparser.ParsedMail, tenantID *int64) {
	s.applyRetention(ctx, id, pm, tenantID)
}

// EffectiveLockActive reports whether any retention lock can currently apply
// system-wide: a positive global default, a positive minimum floor, any tenant
// with retention_days > 0, or any rule carrying a retention_days value. Used by
// the healthcheck (PROJ-51) to warn when effectively no deletion lock is in
// force.
func (s *Store) EffectiveLockActive(ctx context.Context) (bool, error) {
	if s.retentionDays > 0 || s.minRetentionDays > 0 {
		return true, nil
	}
	if s.db == nil {
		return false, nil
	}
	var n int
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM tenants WHERE retention_days > 0`).Scan(&n); err == nil && n > 0 {
		return true, nil
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM archiving_rules WHERE retention_days IS NOT NULL`).Scan(&n); err == nil && n > 0 {
		return true, nil
	}
	return false, nil
}

// GetRetentionInfo returns the stored retain_until and its source marker for a
// single mail (PROJ-51), for the admin/auditor detail view.
func (s *Store) GetRetentionInfo(ctx context.Context, id string) (until *time.Time, source string, err error) {
	if s.db == nil {
		return nil, "", fmt.Errorf("storage: no db")
	}
	var src *string
	err = s.db.QueryRow(ctx,
		`SELECT retain_until, retain_until_source FROM emails WHERE id=$1`, id,
	).Scan(&until, &src)
	if err != nil {
		return nil, "", fmt.Errorf("storage: retention info: %w", err)
	}
	if src != nil {
		source = *src
	}
	return until, source, nil
}
