// Package decisions wraps the decision_log table introduced in Phase 7.
// Recommendations from internal/intelligence are written here; the operator
// reads, acknowledges, applies, or dismisses them via /api/decisions/*.
package decisions

import (
	"context"
	"database/sql"
	"errors"
	"time"
)

// ErrNotFound is returned when the requested decision id is absent.
var ErrNotFound = errors.New("decisions: not found")

// Decision is the typed shape returned by Get / List.
type Decision struct {
	ID                 int64  `json:"id"`
	Timestamp          int64  `json:"timestamp"`
	DecisionType       string `json:"decision_type"`
	SubjectType        string `json:"subject_type"`
	SubjectID          string `json:"subject_id"`
	Recommendation     string `json:"recommendation"`
	Confidence         *float64 `json:"confidence,omitempty"`
	ProvenanceJSON     string `json:"provenance_json"`
	AlternativesJSON   string `json:"alternatives_json,omitempty"`
	OperatorAction     string `json:"operator_action,omitempty"`
	OperatorActionAt   *int64 `json:"operator_action_at,omitempty"`
}

// Store wraps the decision_log table.
type Store struct {
	db *sql.DB
}

// NewStore constructs a Store.
func NewStore(db *sql.DB) *Store { return &Store{db: db} }

// InsertParams is the typed insert shape.
type InsertParams struct {
	DecisionType     string
	SubjectType      string
	SubjectID        string
	Recommendation   string
	Confidence       *float64
	ProvenanceJSON   string
	AlternativesJSON string
}

// Insert persists a decision. Returns the new id.
func (s *Store) Insert(ctx context.Context, p InsertParams) (int64, error) {
	res, err := s.db.ExecContext(ctx, `
		INSERT INTO decision_log
		  (timestamp, decision_type, subject_type, subject_id,
		   recommendation, confidence, provenance_json, alternatives_json)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)
	`,
		time.Now().Unix(),
		p.DecisionType, p.SubjectType, p.SubjectID, p.Recommendation,
		nullFloat(p.Confidence), p.ProvenanceJSON,
		nullStr(p.AlternativesJSON),
	)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// List returns recent decisions, optionally filtered by action state.
//
//   action == ""        → all
//   action == "open"    → operator_action IS NULL
//   action == "applied" → operator_action = 'applied'
//   action == "..."     → operator_action = <value>
func (s *Store) List(ctx context.Context, action string, limit int) ([]Decision, error) {
	if limit <= 0 || limit > 1000 {
		limit = 100
	}
	q := `
		SELECT id, timestamp, decision_type, subject_type, subject_id,
		       recommendation, confidence, provenance_json, alternatives_json,
		       operator_action, operator_action_at
		FROM decision_log
	`
	var args []any
	switch action {
	case "":
	case "open":
		q += ` WHERE operator_action IS NULL`
	default:
		q += ` WHERE operator_action = ?`
		args = append(args, action)
	}
	q += ` ORDER BY timestamp DESC LIMIT ?`
	args = append(args, limit)
	rows, err := s.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []Decision{}
	for rows.Next() {
		var (
			d         Decision
			conf      sql.NullFloat64
			alts      sql.NullString
			action    sql.NullString
			actAt     sql.NullInt64
		)
		if err := rows.Scan(&d.ID, &d.Timestamp, &d.DecisionType, &d.SubjectType, &d.SubjectID,
			&d.Recommendation, &conf, &d.ProvenanceJSON, &alts, &action, &actAt); err != nil {
			return nil, err
		}
		if conf.Valid {
			v := conf.Float64
			d.Confidence = &v
		}
		if alts.Valid {
			d.AlternativesJSON = alts.String
		}
		if action.Valid {
			d.OperatorAction = action.String
		}
		if actAt.Valid {
			v := actAt.Int64
			d.OperatorActionAt = &v
		}
		out = append(out, d)
	}
	return out, rows.Err()
}

// Get fetches one decision by id.
func (s *Store) Get(ctx context.Context, id int64) (Decision, error) {
	row := s.db.QueryRowContext(ctx, `
		SELECT id, timestamp, decision_type, subject_type, subject_id,
		       recommendation, confidence, provenance_json, alternatives_json,
		       operator_action, operator_action_at
		FROM decision_log WHERE id = ?
	`, id)
	var (
		d      Decision
		conf   sql.NullFloat64
		alts   sql.NullString
		action sql.NullString
		actAt  sql.NullInt64
	)
	if err := row.Scan(&d.ID, &d.Timestamp, &d.DecisionType, &d.SubjectType, &d.SubjectID,
		&d.Recommendation, &conf, &d.ProvenanceJSON, &alts, &action, &actAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Decision{}, ErrNotFound
		}
		return Decision{}, err
	}
	if conf.Valid {
		v := conf.Float64
		d.Confidence = &v
	}
	if alts.Valid {
		d.AlternativesJSON = alts.String
	}
	if action.Valid {
		d.OperatorAction = action.String
	}
	if actAt.Valid {
		v := actAt.Int64
		d.OperatorActionAt = &v
	}
	return d, nil
}

// SetAction transitions a decision to acknowledged / applied / dismissed.
// Setting action="" clears the row (used by tests; not exposed in the API).
func (s *Store) SetAction(ctx context.Context, id int64, action string) error {
	var (
		res sql.Result
		err error
	)
	if action == "" {
		res, err = s.db.ExecContext(ctx,
			`UPDATE decision_log SET operator_action = NULL, operator_action_at = NULL WHERE id = ?`, id)
	} else {
		res, err = s.db.ExecContext(ctx,
			`UPDATE decision_log SET operator_action = ?, operator_action_at = ? WHERE id = ?`,
			action, time.Now().Unix(), id)
	}
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func nullFloat(f *float64) any {
	if f == nil {
		return nil
	}
	return *f
}
func nullStr(s string) any {
	if s == "" {
		return nil
	}
	return s
}
