package store

import (
	"context"
	"fmt"
	"strings"

	"github.com/jackc/pgx/v5"

	"git.warky.dev/wdevs/amcs/internal/generatedmodels"
	ext "git.warky.dev/wdevs/amcs/internal/types"
)

const planColumns = `
	id, guid, title, description, status, priority, project_id, owner, due_date,
	completed_at, reviewed_by, last_reviewed_at, supersedes_plan_id, tags::text[], created_at, updated_at`

func (db *DB) CreatePlan(ctx context.Context, plan ext.Plan) (ext.Plan, error) {
	row := db.pool.QueryRow(ctx, `
		insert into plans (title, description, status, priority, project_id, owner, due_date,
			completed_at, reviewed_by, last_reviewed_at, supersedes_plan_id, tags)
		values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
		returning`+planColumns,
		strings.TrimSpace(plan.Title),
		strings.TrimSpace(plan.Description),
		string(plan.Status),
		string(plan.Priority),
		plan.ProjectID,
		nullableText(plan.Owner),
		plan.DueDate,
		plan.CompletedAt,
		nullableText(plan.ReviewedBy),
		plan.LastReviewedAt,
		plan.SupersedesPlanID,
		plan.Tags,
	)
	return scanPlan(row)
}

func (db *DB) GetPlan(ctx context.Context, id int64) (ext.Plan, error) {
	row := db.pool.QueryRow(ctx, `select`+planColumns+` from plans where id = $1`, id)
	plan, err := scanPlan(row)
	if err != nil {
		if err == pgx.ErrNoRows {
			return ext.Plan{}, fmt.Errorf("plan not found: %d", id)
		}
		return ext.Plan{}, fmt.Errorf("get plan: %w", err)
	}
	return plan, nil
}

func (db *DB) GetPlanDetail(ctx context.Context, id int64) (ext.PlanDetail, error) {
	plan, err := db.GetPlan(ctx, id)
	if err != nil {
		return ext.PlanDetail{}, err
	}

	dependsOn, err := db.listPlansByQuery(ctx, `
		select`+planColumns+`
		from plans p
		join plan_dependencies pd on pd.depends_on_plan_id = p.id
		where pd.plan_id = $1 order by p.title`, id)
	if err != nil {
		return ext.PlanDetail{}, fmt.Errorf("get plan depends_on: %w", err)
	}

	blocks, err := db.listPlansByQuery(ctx, `
		select`+planColumns+`
		from plans p
		join plan_dependencies pd on pd.plan_id = p.id
		where pd.depends_on_plan_id = $1 order by p.title`, id)
	if err != nil {
		return ext.PlanDetail{}, fmt.Errorf("get plan blocks: %w", err)
	}

	related, err := db.listPlansByQuery(ctx, `
		select`+planColumns+`
		from plans p
		where p.id in (
			select plan_b_id from plan_related_plans where plan_a_id = $1
			union
			select plan_a_id from plan_related_plans where plan_b_id = $1
		) order by p.title`, id)
	if err != nil {
		return ext.PlanDetail{}, fmt.Errorf("get plan related: %w", err)
	}

	skills, err := db.ListPlanSkills(ctx, id)
	if err != nil {
		return ext.PlanDetail{}, fmt.Errorf("get plan skills: %w", err)
	}

	guardrails, err := db.ListPlanGuardrails(ctx, id)
	if err != nil {
		return ext.PlanDetail{}, fmt.Errorf("get plan guardrails: %w", err)
	}

	return ext.PlanDetail{
		Plan:         plan,
		DependsOn:    dependsOn,
		Blocks:       blocks,
		RelatedPlans: related,
		Skills:       skills,
		Guardrails:   guardrails,
	}, nil
}

func (db *DB) UpdatePlan(ctx context.Context, id int64, u ext.PlanUpdate) (ext.Plan, error) {
	sets := []string{"updated_at = now()"}
	args := []any{}

	if u.Title != nil {
		args = append(args, strings.TrimSpace(*u.Title))
		sets = append(sets, fmt.Sprintf("title = $%d", len(args)))
	}
	if u.Description != nil {
		args = append(args, strings.TrimSpace(*u.Description))
		sets = append(sets, fmt.Sprintf("description = $%d", len(args)))
	}
	if u.Status != nil {
		args = append(args, strings.TrimSpace(*u.Status))
		sets = append(sets, fmt.Sprintf("status = $%d", len(args)))
	}
	if u.Priority != nil {
		args = append(args, strings.TrimSpace(*u.Priority))
		sets = append(sets, fmt.Sprintf("priority = $%d", len(args)))
	}
	if u.Owner != nil {
		args = append(args, nullableText(*u.Owner))
		sets = append(sets, fmt.Sprintf("owner = $%d", len(args)))
	}
	if u.ClearDueDate {
		sets = append(sets, "due_date = null")
	} else if u.DueDate != nil {
		args = append(args, *u.DueDate)
		sets = append(sets, fmt.Sprintf("due_date = $%d", len(args)))
	}
	if u.ClearCompletedAt {
		sets = append(sets, "completed_at = null")
	} else if u.CompletedAt != nil {
		args = append(args, *u.CompletedAt)
		sets = append(sets, fmt.Sprintf("completed_at = $%d", len(args)))
	}
	if u.ReviewedBy != nil {
		args = append(args, nullableText(*u.ReviewedBy))
		sets = append(sets, fmt.Sprintf("reviewed_by = $%d", len(args)))
	}
	if u.MarkReviewed {
		sets = append(sets, "last_reviewed_at = now()")
	}
	if u.ClearSupersedesPlanID {
		sets = append(sets, "supersedes_plan_id = null")
	} else if u.SupersedesPlanID != nil {
		args = append(args, *u.SupersedesPlanID)
		sets = append(sets, fmt.Sprintf("supersedes_plan_id = $%d", len(args)))
	}
	if u.Tags != nil {
		args = append(args, *u.Tags)
		sets = append(sets, fmt.Sprintf("tags = $%d", len(args)))
	}

	args = append(args, id)
	query := fmt.Sprintf(
		"update plans set %s where id = $%d returning%s",
		strings.Join(sets, ", "), len(args), planColumns,
	)

	row := db.pool.QueryRow(ctx, query, args...)
	plan, err := scanPlan(row)
	if err != nil {
		if err == pgx.ErrNoRows {
			return ext.Plan{}, fmt.Errorf("plan not found: %d", id)
		}
		return ext.Plan{}, fmt.Errorf("update plan: %w", err)
	}
	return plan, nil
}

func (db *DB) DeletePlan(ctx context.Context, id int64) error {
	tag, err := db.pool.Exec(ctx, `delete from plans where id = $1`, id)
	if err != nil {
		return fmt.Errorf("delete plan: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("plan not found")
	}
	return nil
}

func (db *DB) ListPlans(ctx context.Context, filter ext.PlanFilter) ([]ext.Plan, error) {
	args := make([]any, 0, 8)
	conditions := make([]string, 0, 8)

	if filter.ProjectID != nil {
		args = append(args, *filter.ProjectID)
		conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
	}
	if v := strings.TrimSpace(filter.Status); v != "" {
		args = append(args, v)
		conditions = append(conditions, fmt.Sprintf("status = $%d", len(args)))
	}
	if v := strings.TrimSpace(filter.Priority); v != "" {
		args = append(args, v)
		conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args)))
	}
	if v := strings.TrimSpace(filter.Owner); v != "" {
		args = append(args, v)
		conditions = append(conditions, fmt.Sprintf("owner = $%d", len(args)))
	}
	if v := strings.TrimSpace(filter.Tag); v != "" {
		args = append(args, v)
		conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
	}
	if v := strings.TrimSpace(filter.Query); v != "" {
		args = append(args, v)
		conditions = append(conditions, fmt.Sprintf(
			"to_tsvector('simple', title || ' ' || coalesce(description, '')) @@ websearch_to_tsquery('simple', $%d)", len(args)))
	}

	query := "select" + planColumns + " from plans"
	if len(conditions) > 0 {
		query += " where " + strings.Join(conditions, " and ")
	}
	query += " order by updated_at desc"
	if filter.Limit > 0 {
		args = append(args, filter.Limit)
		query += fmt.Sprintf(" limit $%d", len(args))
	}

	return db.listPlansByQuery(ctx, query, args...)
}

// Dependencies

func (db *DB) AddPlanDependency(ctx context.Context, planID, dependsOnPlanID int64) error {
	_, err := db.pool.Exec(ctx, `
		insert into plan_dependencies (plan_id, depends_on_plan_id)
		values ($1, $2)
		on conflict do nothing
	`, planID, dependsOnPlanID)
	if err != nil {
		return fmt.Errorf("add plan dependency: %w", err)
	}
	return nil
}

func (db *DB) RemovePlanDependency(ctx context.Context, planID, dependsOnPlanID int64) error {
	tag, err := db.pool.Exec(ctx, `
		delete from plan_dependencies where plan_id = $1 and depends_on_plan_id = $2
	`, planID, dependsOnPlanID)
	if err != nil {
		return fmt.Errorf("remove plan dependency: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("plan dependency not found")
	}
	return nil
}

// Related Plans

func (db *DB) AddRelatedPlan(ctx context.Context, planAID, planBID int64) error {
	a, b := canonicalPlanPair(planAID, planBID)
	_, err := db.pool.Exec(ctx, `
		insert into plan_related_plans (plan_a_id, plan_b_id)
		values ($1, $2)
		on conflict do nothing
	`, a, b)
	if err != nil {
		return fmt.Errorf("add related plan: %w", err)
	}
	return nil
}

func (db *DB) RemoveRelatedPlan(ctx context.Context, planAID, planBID int64) error {
	a, b := canonicalPlanPair(planAID, planBID)
	tag, err := db.pool.Exec(ctx, `
		delete from plan_related_plans where plan_a_id = $1 and plan_b_id = $2
	`, a, b)
	if err != nil {
		return fmt.Errorf("remove related plan: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("related plan link not found")
	}
	return nil
}

// Plan Skills

func (db *DB) AddPlanSkill(ctx context.Context, planID, skillID int64) error {
	_, err := db.pool.Exec(ctx, `
		insert into plan_skills (plan_id, skill_id) values ($1, $2) on conflict do nothing
	`, planID, skillID)
	if err != nil {
		return fmt.Errorf("add plan skill: %w", err)
	}
	return nil
}

func (db *DB) RemovePlanSkill(ctx context.Context, planID, skillID int64) error {
	tag, err := db.pool.Exec(ctx, `
		delete from plan_skills where plan_id = $1 and skill_id = $2
	`, planID, skillID)
	if err != nil {
		return fmt.Errorf("remove plan skill: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("plan skill link not found")
	}
	return nil
}

func (db *DB) ListPlanSkills(ctx context.Context, planID int64) ([]ext.AgentSkill, error) {
	rows, err := db.pool.Query(ctx, `
		select s.id, s.name, s.description, s.content, s.tags::text[], s.created_at, s.updated_at
		from agent_skills s
		join plan_skills ps on ps.skill_id = s.id
		where ps.plan_id = $1
		order by s.name
	`, planID)
	if err != nil {
		return nil, fmt.Errorf("list plan skills: %w", err)
	}
	defer rows.Close()

	var skills []ext.AgentSkill
	for rows.Next() {
		var model generatedmodels.ModelPublicAgentSkills
		var tags []string
		if err := rows.Scan(&model.ID, &model.Name, &model.Description, &model.Content, &tags, &model.CreatedAt, &model.UpdatedAt); err != nil {
			return nil, fmt.Errorf("scan plan skill: %w", err)
		}
		s := ext.AgentSkill{
			ID:          model.ID.Int64(),
			Name:        model.Name.String(),
			Description: model.Description.String(),
			Content:     model.Content.String(),
			Tags:        tags,
			CreatedAt:   model.CreatedAt.Time(),
			UpdatedAt:   model.UpdatedAt.Time(),
		}
		if s.Tags == nil {
			s.Tags = []string{}
		}
		skills = append(skills, s)
	}
	return skills, rows.Err()
}

// Plan Guardrails

func (db *DB) AddPlanGuardrail(ctx context.Context, planID, guardrailID int64) error {
	_, err := db.pool.Exec(ctx, `
		insert into plan_guardrails (plan_id, guardrail_id) values ($1, $2) on conflict do nothing
	`, planID, guardrailID)
	if err != nil {
		return fmt.Errorf("add plan guardrail: %w", err)
	}
	return nil
}

func (db *DB) RemovePlanGuardrail(ctx context.Context, planID, guardrailID int64) error {
	tag, err := db.pool.Exec(ctx, `
		delete from plan_guardrails where plan_id = $1 and guardrail_id = $2
	`, planID, guardrailID)
	if err != nil {
		return fmt.Errorf("remove plan guardrail: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("plan guardrail link not found")
	}
	return nil
}

func (db *DB) ListPlanGuardrails(ctx context.Context, planID int64) ([]ext.AgentGuardrail, error) {
	rows, err := db.pool.Query(ctx, `
		select g.id, g.name, g.description, g.content, g.severity, g.tags::text[], g.created_at, g.updated_at
		from agent_guardrails g
		join plan_guardrails pg on pg.guardrail_id = g.id
		where pg.plan_id = $1
		order by g.name
	`, planID)
	if err != nil {
		return nil, fmt.Errorf("list plan guardrails: %w", err)
	}
	defer rows.Close()

	var guardrails []ext.AgentGuardrail
	for rows.Next() {
		var model generatedmodels.ModelPublicAgentGuardrails
		var tags []string
		if err := rows.Scan(&model.ID, &model.Name, &model.Description, &model.Content, &model.Severity, &tags, &model.CreatedAt, &model.UpdatedAt); err != nil {
			return nil, fmt.Errorf("scan plan guardrail: %w", err)
		}
		g := ext.AgentGuardrail{
			ID:          model.ID.Int64(),
			Name:        model.Name.String(),
			Description: model.Description.String(),
			Content:     model.Content.String(),
			Severity:    model.Severity.String(),
			Tags:        tags,
			CreatedAt:   model.CreatedAt.Time(),
			UpdatedAt:   model.UpdatedAt.Time(),
		}
		if g.Tags == nil {
			g.Tags = []string{}
		}
		guardrails = append(guardrails, g)
	}
	return guardrails, rows.Err()
}

// helpers

type planScanner interface {
	Scan(dest ...any) error
}

func scanPlan(row planScanner) (ext.Plan, error) {
	var model generatedmodels.ModelPublicPlans
	var tags []string
	err := row.Scan(
		&model.ID,
		&model.GUID,
		&model.Title,
		&model.Description,
		&model.Status,
		&model.Priority,
		&model.ProjectID,
		&model.Owner,
		&model.DueDate,
		&model.CompletedAt,
		&model.ReviewedBy,
		&model.LastReviewedAt,
		&model.SupersedesPlanID,
		&tags,
		&model.CreatedAt,
		&model.UpdatedAt,
	)
	if err != nil {
		return ext.Plan{}, err
	}
	if tags == nil {
		tags = []string{}
	}
	return planFromModel(model, tags), nil
}

func (db *DB) listPlansByQuery(ctx context.Context, query string, args ...any) ([]ext.Plan, error) {
	rows, err := db.pool.Query(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	plans := make([]ext.Plan, 0)
	for rows.Next() {
		plan, err := scanPlan(rows)
		if err != nil {
			return nil, fmt.Errorf("scan plan: %w", err)
		}
		plans = append(plans, plan)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate plans: %w", err)
	}
	return plans, nil
}

// canonicalPlanPair ensures the smaller ID is always plan_a_id to prevent duplicates.
func canonicalPlanPair(a, b int64) (int64, int64) {
	if a <= b {
		return a, b
	}
	return b, a
}

