package services

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"

	"mgit.msbls.de/m/paliad/internal/models"
	lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
)

// ScenarioService reads + writes paliad.scenarios — named compositions
// of existing proceedings + flags + per-card choices + anchor dates,
// switchable per project or saved as abstract templates on
// /tools/verfahrensablauf. Slice D, m/paliad#124 §5, mig 145.
//
// Visibility:
//   - Project-scoped scenarios (project_id NOT NULL): require
//     can_see_project on the bound project (mirrors
//     EventChoiceService.requireProjectVisible).
//   - Abstract scenarios (project_id IS NULL): owner-only. Only
//     created_by can read / mutate.
//
// The service applies these checks in application code; paliad.scenarios
// also has RLS policies (mig 145) as defense-in-depth for callers that
// connect through Supabase Auth's auth.uid() session.
type ScenarioService struct {
	db       *sqlx.DB
	projects *ProjectService
	rules    *DeadlineRuleService
}

// NewScenarioService wires the service to its dependencies.
func NewScenarioService(db *sqlx.DB, projects *ProjectService, rules *DeadlineRuleService) *ScenarioService {
	return &ScenarioService{db: db, projects: projects, rules: rules}
}

// Sentinel errors. Mirrors EventChoiceService + the lp package errors
// so handlers can map cleanly to HTTP statuses.
var (
	ErrScenarioNotVisible = errors.New("scenario not visible to caller")
)

// CreateScenarioInput is the payload for POST /api/scenarios. project_id
// nil = abstract scenario (saved Verfahrensablauf template).
type CreateScenarioInput struct {
	ProjectID   *uuid.UUID      `json:"project_id,omitempty"`
	Name        string          `json:"name"`
	Description *string         `json:"description,omitempty"`
	Spec        json.RawMessage `json:"spec"`
}

// Create inserts a new scenario after validating the spec.
func (s *ScenarioService) Create(ctx context.Context, userID uuid.UUID, input CreateScenarioInput) (*lp.Scenario, error) {
	if input.Name == "" {
		return nil, fmt.Errorf("%w: name required", ErrInvalidInput)
	}
	if err := s.validateSpec(ctx, input.Spec); err != nil {
		return nil, err
	}
	if input.ProjectID != nil {
		if err := s.requireProjectVisible(ctx, userID, *input.ProjectID); err != nil {
			return nil, err
		}
	}

	var out lp.Scenario
	err := s.db.GetContext(ctx, &out,
		`INSERT INTO paliad.scenarios (project_id, name, description, spec, created_by)
		      VALUES ($1, $2, $3, $4, $5)
		   RETURNING id, project_id, name, description, spec, created_by,
		             created_at, updated_at`,
		input.ProjectID, input.Name, input.Description,
		[]byte(input.Spec), userID)
	if err != nil {
		return nil, fmt.Errorf("create scenario: %w", err)
	}
	return &out, nil
}

// Get returns one scenario by id after a visibility check.
func (s *ScenarioService) Get(ctx context.Context, userID, scenarioID uuid.UUID) (*lp.Scenario, error) {
	var sc lp.Scenario
	err := s.db.GetContext(ctx, &sc,
		`SELECT id, project_id, name, description, spec, created_by,
		        created_at, updated_at
		   FROM paliad.scenarios
		  WHERE id = $1`, scenarioID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, lp.ErrUnknownScenario
	}
	if err != nil {
		return nil, fmt.Errorf("get scenario: %w", err)
	}
	if err := s.requireVisible(ctx, userID, &sc); err != nil {
		return nil, err
	}
	return &sc, nil
}

// ListForProject returns scenarios attached to one project, ordered by
// created_at desc.
func (s *ScenarioService) ListForProject(ctx context.Context, userID, projectID uuid.UUID) ([]lp.Scenario, error) {
	if err := s.requireProjectVisible(ctx, userID, projectID); err != nil {
		return nil, err
	}
	out := []lp.Scenario{}
	err := s.db.SelectContext(ctx, &out,
		`SELECT id, project_id, name, description, spec, created_by,
		        created_at, updated_at
		   FROM paliad.scenarios
		  WHERE project_id = $1
		  ORDER BY created_at DESC`, projectID)
	if err != nil {
		return nil, fmt.Errorf("list scenarios for project: %w", err)
	}
	return out, nil
}

// ListAbstractForUser returns the calling user's abstract scenarios.
func (s *ScenarioService) ListAbstractForUser(ctx context.Context, userID uuid.UUID) ([]lp.Scenario, error) {
	out := []lp.Scenario{}
	err := s.db.SelectContext(ctx, &out,
		`SELECT id, project_id, name, description, spec, created_by,
		        created_at, updated_at
		   FROM paliad.scenarios
		  WHERE project_id IS NULL AND created_by = $1
		  ORDER BY created_at DESC`, userID)
	if err != nil {
		return nil, fmt.Errorf("list abstract scenarios: %w", err)
	}
	return out, nil
}

// PatchScenarioInput is the payload for PATCH /api/scenarios/{id}. Any
// field nil means "don't change". Spec replacement re-runs validation.
type PatchScenarioInput struct {
	Name        *string         `json:"name,omitempty"`
	Description *string         `json:"description,omitempty"`
	Spec        json.RawMessage `json:"spec,omitempty"`
}

// Patch updates one or more scenario fields. Visibility check fires
// first (the caller must already see the scenario to mutate it).
func (s *ScenarioService) Patch(ctx context.Context, userID, scenarioID uuid.UUID, input PatchScenarioInput) (*lp.Scenario, error) {
	current, err := s.Get(ctx, userID, scenarioID)
	if err != nil {
		return nil, err
	}
	if len(input.Spec) > 0 {
		if err := s.validateSpec(ctx, input.Spec); err != nil {
			return nil, err
		}
	}

	sets := []string{}
	args := []any{}
	add := func(clause string, val any) {
		args = append(args, val)
		sets = append(sets, fmt.Sprintf(clause, len(args)))
	}
	if input.Name != nil {
		add("name = $%d", *input.Name)
	}
	if input.Description != nil {
		add("description = $%d", *input.Description)
	}
	if len(input.Spec) > 0 {
		add("spec = $%d", []byte(input.Spec))
	}
	if len(sets) == 0 {
		return current, nil
	}
	args = append(args, scenarioID)
	query := fmt.Sprintf(`UPDATE paliad.scenarios SET %s
	    WHERE id = $%d
	RETURNING id, project_id, name, description, spec, created_by,
	          created_at, updated_at`, joinSets(sets), len(args))
	var out lp.Scenario
	if err := s.db.GetContext(ctx, &out, query, args...); err != nil {
		return nil, fmt.Errorf("patch scenario: %w", err)
	}
	return &out, nil
}

// SetActive points a project at one of its scenarios. Pass nil to
// clear (revert to ad-hoc per-card choice state).
func (s *ScenarioService) SetActive(ctx context.Context, userID, projectID uuid.UUID, scenarioID *uuid.UUID) error {
	if err := s.requireProjectVisible(ctx, userID, projectID); err != nil {
		return err
	}
	if scenarioID != nil {
		// Ensure scenario exists + belongs to this project. A scenario
		// from a different project (or an abstract one) can't be the
		// active scenario on this project.
		sc, err := s.Get(ctx, userID, *scenarioID)
		if err != nil {
			return err
		}
		if sc.ProjectID == nil || *sc.ProjectID != projectID {
			return fmt.Errorf("%w: scenario %s is not attached to project %s",
				ErrInvalidInput, *scenarioID, projectID)
		}
	}
	_, err := s.db.ExecContext(ctx,
		`UPDATE paliad.projects SET active_scenario_id = $1 WHERE id = $2`,
		scenarioID, projectID)
	if err != nil {
		return fmt.Errorf("set active scenario: %w", err)
	}
	return nil
}

// Delete removes a scenario. Project's active_scenario_id is cleared
// automatically via the FK's ON DELETE SET NULL.
func (s *ScenarioService) Delete(ctx context.Context, userID, scenarioID uuid.UUID) error {
	// Visibility check via Get — also resolves the existence question.
	if _, err := s.Get(ctx, userID, scenarioID); err != nil {
		return err
	}
	if _, err := s.db.ExecContext(ctx,
		`DELETE FROM paliad.scenarios WHERE id = $1`, scenarioID); err != nil {
		return fmt.Errorf("delete scenario: %w", err)
	}
	return nil
}

// requireVisible enforces the per-row visibility rule:
//   - project_id NOT NULL → caller must see the project
//   - project_id IS NULL  → caller must be the row's created_by
func (s *ScenarioService) requireVisible(ctx context.Context, userID uuid.UUID, sc *lp.Scenario) error {
	if sc.ProjectID != nil {
		return s.requireProjectVisible(ctx, userID, *sc.ProjectID)
	}
	if sc.CreatedBy == nil || *sc.CreatedBy != userID {
		return ErrScenarioNotVisible
	}
	return nil
}

// requireProjectVisible mirrors EventChoiceService.requireProjectVisible
// (visibility via can_see_project). Cheap re-implementation — keeps the
// call-graph small + avoids a cross-service dep.
func (s *ScenarioService) requireProjectVisible(ctx context.Context, userID, projectID uuid.UUID) error {
	var visible bool
	err := s.db.GetContext(ctx, &visible,
		`SELECT EXISTS (
		    SELECT 1 FROM paliad.users u
		     WHERE u.id = $1 AND u.global_role = 'global_admin'
		) OR EXISTS (
		    SELECT 1 FROM paliad.projects p
		     JOIN paliad.project_teams pt ON pt.project_id = ANY(
		            string_to_array(p.path, '.')::uuid[]
		          )
		    WHERE p.id = $2 AND pt.user_id = $1
		)`, userID, projectID)
	if err != nil {
		return fmt.Errorf("check project visibility: %w", err)
	}
	if !visible {
		return ErrScenarioNotVisible
	}
	return nil
}

// validateSpec checks the jsonb spec is well-formed, has the right
// version, and that every referenced proceeding code + submission code
// resolves to an active row in the live catalog. Surfaces friendly
// errors wrapping ErrInvalidInput so the handler can map to a 400.
func (s *ScenarioService) validateSpec(ctx context.Context, raw json.RawMessage) error {
	if len(raw) == 0 {
		return fmt.Errorf("%w: spec is required", ErrInvalidInput)
	}
	parsed, err := lp.ParseSpec(lp.NullableJSON(raw))
	if err != nil {
		return fmt.Errorf("%w: %v", ErrInvalidInput, err)
	}
	if _, err := parsed.PrimaryProceeding(); err != nil {
		return fmt.Errorf("%w: %v", ErrInvalidInput, err)
	}
	if parsed.BaseTriggerDate != "" {
		if _, err := time.Parse("2006-01-02", parsed.BaseTriggerDate); err != nil {
			return fmt.Errorf("%w: base_trigger_date %q is not YYYY-MM-DD", ErrInvalidInput, parsed.BaseTriggerDate)
		}
	}
	for i, p := range parsed.Proceedings {
		if p.Code == "" {
			return fmt.Errorf("%w: proceedings[%d].code is empty", ErrInvalidInput, i)
		}
		if p.Role != lp.ScenarioRolePrimary && p.Role != lp.ScenarioRolePeer {
			return fmt.Errorf("%w: proceedings[%d].role=%q must be 'primary' or 'peer'",
				ErrInvalidInput, i, p.Role)
		}
		if p.AppealTarget != "" && !lp.IsValidAppealTarget(p.AppealTarget) {
			return fmt.Errorf("%w: proceedings[%d].appeal_target=%q not in %v",
				ErrInvalidInput, i, p.AppealTarget, lp.AppealTargets)
		}
		if p.TriggerDateOverride != "" {
			if _, err := time.Parse("2006-01-02", p.TriggerDateOverride); err != nil {
				return fmt.Errorf("%w: proceedings[%d].trigger_date_override %q is not YYYY-MM-DD",
					ErrInvalidInput, i, p.TriggerDateOverride)
			}
		}
		for code, dateStr := range p.AnchorOverrides {
			if _, err := time.Parse("2006-01-02", dateStr); err != nil {
				return fmt.Errorf("%w: proceedings[%d].anchor_overrides[%q]=%q is not YYYY-MM-DD",
					ErrInvalidInput, i, code, dateStr)
			}
		}
		// Resolve code against active proceedings.
		var exists bool
		if err := s.db.GetContext(ctx, &exists,
			`SELECT EXISTS(SELECT 1 FROM paliad.proceeding_types
			               WHERE code = $1 AND is_active = true)`,
			p.Code); err != nil {
			return fmt.Errorf("validate spec proceedings[%d]: %w", i, err)
		}
		if !exists {
			return fmt.Errorf("%w: proceedings[%d].code=%q is not an active proceeding_type",
				ErrInvalidInput, i, p.Code)
		}
	}
	return nil
}

// joinSets joins SET clauses with ", ". Tiny utility, kept here to
// avoid cross-package strings.Join indirection.
func joinSets(sets []string) string {
	out := ""
	for i, s := range sets {
		if i > 0 {
			out += ", "
		}
		out += s
	}
	return out
}

// Suppress unused-import diagnostic when models isn't referenced
// (kept for future shape-evolution; canonical scenario row lives in lp).
var _ = models.NullableJSON(nil)
