package services

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/lib/pq"
)

// Slice 11b orphan-resolution flow (t-paliad-192).
//
// Slice 10 (mig 089) staged the legacy paliad.deadlines rows that the
// fuzzy-match backfill couldn't bind uniquely to a deadline_rule. This
// file surfaces those rows to the admin rule-editor UI so a human can
// pick the right rule from the candidate list and write rule_id back
// onto the deadline.
//
// The methods sit on RuleEditorService because the orphan flow is part
// of the same admin surface and shares the same audit semantics — the
// resolved_rule_id + resolved_at pair on the staging row IS the audit
// trail. No new DB trigger needed; the staging table doubles as the
// log of the legal-review pass per mig 089's COMMENT.

// ErrOrphanAlreadyResolved is returned when a resolve call hits a row
// whose resolved_at is already non-NULL. 409 Conflict in the handler so
// the editor can re-fetch and show the picker the other admin made.
var ErrOrphanAlreadyResolved = errors.New("orphan already resolved")

// ErrOrphanCandidateMismatch is returned when the editor picks a rule
// that is not in the staging row's candidate_rule_ids set. The list of
// candidates is the matcher's output and the only legal choice — to
// pick anything else, an admin should patch the deadline directly.
var ErrOrphanCandidateMismatch = errors.New("rule_id not in candidate set")

// OrphanCandidate is one suggested rule from the fuzzy matcher with the
// fields the editor needs to render the pick chip.
type OrphanCandidate struct {
	ID       uuid.UUID `db:"id"        json:"id"`
	RuleCode *string   `db:"rule_code" json:"rule_code,omitempty"`
	Name     string    `db:"name"      json:"name"`
	NameEN   string    `db:"name_en"   json:"name_en"`
}

// Orphan is one row from paliad.deadline_rule_backfill_orphans hydrated
// with its candidate rule rows (joined from paliad.deadline_rules so
// the UI doesn't need a second round-trip per row).
type Orphan struct {
	ID              uuid.UUID         `json:"id"`
	DeadlineID      uuid.UUID         `json:"deadline_id"`
	Title           string            `json:"title"`
	ProjectID       *uuid.UUID        `json:"project_id,omitempty"`
	ProceedingCode  *string           `json:"proceeding_code,omitempty"`
	Reason          string            `json:"reason"`
	CandidateCount  int               `json:"candidate_count"`
	CandidateIDs    []uuid.UUID       `json:"candidate_ids"`
	Candidates      []OrphanCandidate `json:"candidates"`
	CreatedAt       time.Time         `json:"created_at"`
	ProjectTitle    *string           `json:"project_title,omitempty"`
}

// ListOrphans returns unresolved staging rows newest-first. The fuzzy
// matcher inserted at most ~25 rows so a flat list is fine; pagination
// can be added later if the table ever grows past a screen.
func (s *RuleEditorService) ListOrphans(ctx context.Context) ([]Orphan, error) {
	type row struct {
		ID             uuid.UUID      `db:"id"`
		DeadlineID     uuid.UUID      `db:"deadline_id"`
		Title          string         `db:"title"`
		ProjectID      *uuid.UUID     `db:"project_id"`
		ProceedingCode *string        `db:"proceeding_code"`
		Reason         string         `db:"reason"`
		CandidateCount int            `db:"candidate_count"`
		CandidateIDs   pq.StringArray `db:"candidate_rule_ids"`
		CreatedAt      time.Time      `db:"created_at"`
		ProjectTitle   *string        `db:"project_title"`
	}
	var rows []row
	if err := s.db.SelectContext(ctx, &rows, `
		SELECT o.id, o.deadline_id, o.title, o.project_id, o.proceeding_code,
		       o.reason, o.candidate_count, o.candidate_rule_ids, o.created_at,
		       p.title AS project_title
		  FROM paliad.deadline_rule_backfill_orphans o
		  LEFT JOIN paliad.projects p ON p.id = o.project_id
		 WHERE o.resolved_at IS NULL
		 ORDER BY o.created_at DESC`); err != nil {
		return nil, fmt.Errorf("list orphans: %w", err)
	}

	// Collect every candidate UUID, fetch the rule rows in one shot, then
	// fan back out per orphan. Avoids N+1 SELECTs when the matcher
	// produced ambiguous (≥ 2 candidates) hits.
	idSet := map[uuid.UUID]bool{}
	for _, r := range rows {
		for _, sid := range r.CandidateIDs {
			id, err := uuid.Parse(sid)
			if err != nil {
				continue
			}
			idSet[id] = true
		}
	}
	candidateByID := map[uuid.UUID]OrphanCandidate{}
	if len(idSet) > 0 {
		ids := make([]uuid.UUID, 0, len(idSet))
		for id := range idSet {
			ids = append(ids, id)
		}
		var cs []OrphanCandidate
		uuidStrs := make([]string, len(ids))
		for i, id := range ids {
			uuidStrs[i] = id.String()
		}
		if err := s.db.SelectContext(ctx, &cs, `
			SELECT id, rule_code, name, name_en
			  FROM paliad.deadline_rules_unified
			 WHERE id = ANY($1::uuid[])`, pq.Array(uuidStrs)); err != nil {
			return nil, fmt.Errorf("list orphan candidate rules: %w", err)
		}
		for _, c := range cs {
			candidateByID[c.ID] = c
		}
	}

	out := make([]Orphan, 0, len(rows))
	for _, r := range rows {
		cids := make([]uuid.UUID, 0, len(r.CandidateIDs))
		cs := make([]OrphanCandidate, 0, len(r.CandidateIDs))
		for _, sid := range r.CandidateIDs {
			id, err := uuid.Parse(sid)
			if err != nil {
				continue
			}
			cids = append(cids, id)
			if c, ok := candidateByID[id]; ok {
				cs = append(cs, c)
			}
		}
		out = append(out, Orphan{
			ID:             r.ID,
			DeadlineID:     r.DeadlineID,
			Title:          r.Title,
			ProjectID:      r.ProjectID,
			ProceedingCode: r.ProceedingCode,
			Reason:         r.Reason,
			CandidateCount: r.CandidateCount,
			CandidateIDs:   cids,
			Candidates:     cs,
			CreatedAt:      r.CreatedAt,
			ProjectTitle:   r.ProjectTitle,
		})
	}
	return out, nil
}

// ResolveOrphan binds the orphan's deadline to the picked rule_id and
// flips resolved_at + resolved_rule_id on the staging row. Both writes
// land in the same tx; if either fails, the orphan stays open so the
// editor can retry.
//
// reason is captured into paliad.audit_reason so any future audit trigger
// on paliad.deadlines picks it up. As of Slice 11b there is no trigger
// on deadlines (see mig 089 COMMENT), but the session setting is cheap
// to maintain and future-proofs the call site.
func (s *RuleEditorService) ResolveOrphan(ctx context.Context, orphanID uuid.UUID, ruleID uuid.UUID, reason string) error {
	if strings.TrimSpace(reason) == "" {
		return ErrAuditReasonRequired
	}

	type orphanCheck struct {
		DeadlineID   uuid.UUID      `db:"deadline_id"`
		ResolvedAt   *time.Time     `db:"resolved_at"`
		CandidateIDs pq.StringArray `db:"candidate_rule_ids"`
	}
	var oc orphanCheck
	err := s.db.GetContext(ctx, &oc,
		`SELECT deadline_id, resolved_at, candidate_rule_ids
		   FROM paliad.deadline_rule_backfill_orphans
		  WHERE id = $1`, orphanID)
	if errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("%w: orphan %s", ErrRuleNotFound, orphanID)
	}
	if err != nil {
		return fmt.Errorf("load orphan %s: %w", orphanID, err)
	}
	if oc.ResolvedAt != nil {
		return ErrOrphanAlreadyResolved
	}
	inSet := false
	for _, sid := range oc.CandidateIDs {
		id, parseErr := uuid.Parse(sid)
		if parseErr == nil && id == ruleID {
			inSet = true
			break
		}
	}
	if !inSet {
		return ErrOrphanCandidateMismatch
	}

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()
	if err := setAuditReasonTx(ctx, tx, reason); err != nil {
		return err
	}

	now := time.Now().UTC()
	// Slice B.4 (mig 140, t-paliad-305): paliad.deadlines.rule_id column
	// dropped. Back-link lives on sequencing_rule_id (same UUIDs as
	// before — sr.id inherited dr.id at mig 136 backfill).
	// procedural_event_id is derived from the same sequencing_rules row.
	if _, err := tx.ExecContext(ctx,
		`UPDATE paliad.deadlines d
		    SET sequencing_rule_id  = $1,
		        procedural_event_id = (SELECT procedural_event_id
		                                  FROM paliad.sequencing_rules
		                                 WHERE id = $1),
		        updated_at          = $2
		  WHERE d.id = $3`,
		ruleID, now, oc.DeadlineID,
	); err != nil {
		return fmt.Errorf("set deadline sequencing_rule_id: %w", err)
	}
	if _, err := tx.ExecContext(ctx,
		`UPDATE paliad.deadline_rule_backfill_orphans
		    SET resolved_at      = $1,
		        resolved_rule_id = $2
		  WHERE id = $3 AND resolved_at IS NULL`,
		now, ruleID, orphanID,
	); err != nil {
		return fmt.Errorf("mark orphan resolved: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("commit resolve: %w", err)
	}
	return nil
}
