package taskboard

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

var (
	ErrForbidden            = errors.New("forbidden")
	ErrNotFound             = errors.New("not found")
	ErrConflict             = errors.New("conflict")
	ErrBadRequest           = errors.New("bad request")
	ErrInvalidChannelTarget = errors.New("invalid channel target")
)

// InvalidChannelTargetCode is the stable response code for a Task board channel
// reference that is not a usable #channel target. The exec client keys a
// bounded, non-leaking Hint on it instead of guessing from HTTP status.
const InvalidChannelTargetCode = "task_channel_target_invalid"

type requestError struct {
	kind    error
	message string
}

func (err requestError) Error() string {
	return err.message
}

func (err requestError) Unwrap() error {
	return err.kind
}

func requestFailure(kind error, message string) error {
	return requestError{kind: kind, message: message}
}

func badRequest(message string) error {
	return requestFailure(ErrBadRequest, message)
}

func invalidChannelTarget(message string) error {
	return requestFailure(ErrInvalidChannelTarget, message)
}

func conflict(message string) error {
	return requestFailure(ErrConflict, message)
}

// Store owns task board persistence and task-specific rules.
type Store struct {
	db             coredb.DB
	taskChangeSink TaskChangeSink
}

type TxStore struct {
	tx coredb.Tx
	db queryExecer
}

type DeleteTaskSubtaskMode string

const (
	DeleteTaskSubtaskModeUnspecified DeleteTaskSubtaskMode = ""
	DeleteTaskSubtaskModePromote     DeleteTaskSubtaskMode = "promote"
	DeleteTaskSubtaskModeDelete      DeleteTaskSubtaskMode = "delete"
)

// Service is kept as a compatibility alias while callers migrate to Store.
type Service = Store

// StoreOptions configures optional taskboard store hooks.
type StoreOptions struct {
	TaskChangeSink TaskChangeSink
}

func New(db coredb.DB) *Store {
	return &Store{db: db}
}

// NewWithOptions creates a taskboard store with optional transaction hooks.
func NewWithOptions(db coredb.DB, options StoreOptions) *Store {
	return &Store{
		db:             db,
		taskChangeSink: options.TaskChangeSink,
	}
}

func (store *Store) InTx(tx coredb.Tx) TxStore {
	return TxStore{tx: tx, db: tx}
}

type taskBoardRecord struct {
	ID             string
	WorkspaceID    string
	ChannelID      string
	NextTaskNumber int64
}

// AgentActor identifies the agent member operating on a task board.
type AgentActor struct {
	WorkspaceID string
	MemberID    string
}

type TaskActor struct {
	Type string
	ID   string
	Name string
}

type TaskSourceThread struct {
	ThreadChannelID string
	ParentChannelID string
	ParentMessageID string
	Ref             string
}

// Task is the public app-compatible task payload.
type Task struct {
	ID               string
	TaskScopeID      string
	ChannelID        string
	TaskNumber       int64
	Title            string
	Status           string
	Description      string
	Kind             string
	Priority         string
	BlockedReason    string
	LastActivityAt   *time.Time
	ArtifactCount    int64
	ParentTaskRef    string
	DiscussionCount  int64

	AssigneeProfile    *TaskPrincipalProfile
	KindField          *TaskFieldValue
	PriorityField      *TaskFieldValue
	CheckpointProgress TaskProgressSummary
	SubtaskProgress    TaskProgressSummary

	CreatedAt time.Time

	CreatedByProfile *TaskPrincipalProfile
	CreatedByType    string
	CreatedByID      string
	CreatedByName    string

	Source message.ContributionSource
	// SourceThread is retained as the source-thread navigation/projection
	// shape. Canonical task body provenance is Source.
	SourceThread *TaskSourceThread
	ReadState    *TaskReadState

	AssignedAt *time.Time

	CompletedAt *time.Time
}

// TaskPage is the app-compatible page shape for channel tasks.
type TaskPage struct {
	Tasks []Task
}

// TaskView is the daemon-compatible task list item.
type TaskView struct {
	TaskScopeID      string
	TaskNumber       int64
	Title            string
	Status           string
	AssigneeProfile  *TaskPrincipalProfile
	CreatedByProfile *TaskPrincipalProfile
	CreatedByName    string
}

// CreatedTask is the daemon-compatible create result item.
type CreatedTask struct {
	TaskScopeID string
	TaskNumber  int64
	Title       string
	Reused      bool
}

// ClaimResult is the daemon-compatible claim result item.
type ClaimResult struct {
	TaskNumber int64
	Success    bool
	Reason     string
}

type queryExecer = coredb.QueryExecer

func rollback(tx coredb.Tx, ctx context.Context) {
	_ = tx.Rollback(ctx)
}

func (txs TxStore) ensureTaskBoard(ctx context.Context, queryer queryExecer, workspaceID string, channelID string) (taskBoardRecord, error) {
	board, err := txs.loadTaskBoardByChannel(ctx, queryer, workspaceID, channelID)
	if err == nil {
		if err := insertTaskCounterIfMissing(ctx, queryer, board.ID, board.NextTaskNumber, time.Now().UTC()); err != nil {
			return taskBoardRecord{}, err
		}
		return board, nil
	}
	if !errors.Is(err, ErrNotFound) {
		return taskBoardRecord{}, err
	}

	boardID, err := newTaskBoardID()
	if err != nil {
		return taskBoardRecord{}, fmt.Errorf("generate task board id: %w", err)
	}
	now := time.Now().UTC()
	if err := insertTaskBoardIfMissing(ctx, queryer, boardID, workspaceID, channelID, 1, now); err != nil {
		return taskBoardRecord{}, err
	}
	board, err = txs.loadTaskBoardByChannel(ctx, queryer, workspaceID, channelID)
	if err != nil {
		return taskBoardRecord{}, err
	}
	if err := insertTaskCounterIfMissing(ctx, queryer, board.ID, board.NextTaskNumber, now); err != nil {
		return taskBoardRecord{}, err
	}
	return board, nil
}

func (txs TxStore) loadTaskBoardByChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelID string) (taskBoardRecord, error) {
	row := queryer.QueryRow(ctx, `
SELECT id, workspace_id, channel_id, next_task_number
FROM task_boards
WHERE workspace_id = $1 AND channel_id = $2
`, workspaceID, channelID)

	var board taskBoardRecord
	if err := row.Scan(&board.ID, &board.WorkspaceID, &board.ChannelID, &board.NextTaskNumber); err != nil {
		if coredb.IsNotFound(err) {
			return taskBoardRecord{}, ErrNotFound
		}
		return taskBoardRecord{}, fmt.Errorf("load task board: %w", err)
	}
	return board, nil
}

func (txs TxStore) allocateTaskNumbers(ctx context.Context, tx coredb.Tx, board taskBoardRecord, count int) (int64, error) {
	return allocateTaskNumbersWithExecer(ctx, tx, board, count, time.Now().UTC())
}

type taskInsertInput struct {
	Title                  string
	Description            string
	AssigneeMemberID       string
	ParentTaskRef          string
	ExecutionOwnerMemberID string
	KindUpdate             resolvedTaskFieldUpdate
	PriorityUpdate         resolvedTaskFieldUpdate
	Metadata               CreateTaskMetadata
}

func (txs TxStore) insertTask(ctx context.Context, tx coredb.Tx, board taskBoardRecord, taskNumber int64, createdByRef string, input taskInsertInput, createdAt time.Time) (string, string, error) {
	taskID, err := newTaskID()
	if err != nil {
		return "", "", fmt.Errorf("generate task id: %w", err)
	}
	taskScopeID, err := txs.ensureTaskScope(ctx, tx, board.WorkspaceID, taskID, createdByRef, createdAt)
	if err != nil {
		return "", "", err
	}
	executionOwnerMemberID := strings.TrimSpace(input.ExecutionOwnerMemberID)
	if executionOwnerMemberID == "" {
		executionOwnerMemberID, err = resolveTaskExecutionOwnerMemberID(ctx, tx, board.ChannelID, createdByRef)
		if err != nil {
			return "", "", err
		}
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO tasks (
    id,
    task_scope_id,
    board_id,
    channel_id,
    task_number,
    title,
    status,
    description,
    created_by_member_id,
    created_by_ref,
    execution_owner_member_id,
    assignee_member_id,
    claimed_by_member_id,
    claimed_by_ref,
    claimed_at,
    current_agent_ref,
    suggested_assignee_ref,
    reviewer_ref,
    kind,
    kind_option_id,
    legacy_kind_value,
    priority,
    priority_option_id,
    legacy_priority_value,
    source_thread_channel_id,
    source_thread_parent_channel_id,
    source_thread_parent_message_id,
    source_thread_ref,
    parent_task_ref,
    created_at,
    assigned_at,
    last_activity_at,
    updated_at
) VALUES (
    $1, $2, $3, $4, $5, $6, 'todo', $7, $8, $8, $9,
    NULLIF($10, ''), NULLIF($10, ''), NULLIF($10, ''),
    CASE WHEN $10 = '' THEN NULL ELSE $22 END, NULL, '', '',
    $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22,
    CASE WHEN $10 = '' THEN NULL ELSE $22 END, $22, $22
)
`,
		taskID,
		taskScopeID,
		board.ID,
		board.ChannelID,
		taskNumber,
		input.Title,
		input.Description,
		createdByRef,
		executionOwnerMemberID,
		input.AssigneeMemberID,
		input.KindUpdate.EchoValue,
		input.KindUpdate.OptionID,
		input.KindUpdate.LegacyValue,
		input.PriorityUpdate.EchoValue,
		input.PriorityUpdate.OptionID,
		input.PriorityUpdate.LegacyValue,
		input.Metadata.SourceThreadChannelID,
		input.Metadata.SourceThreadParentChannelID,
		input.Metadata.SourceThreadParentMessageID,
		input.Metadata.SourceThreadRef,
		input.ParentTaskRef,
		createdAt,
	); err != nil {
		return "", "", fmt.Errorf("insert task: %w", err)
	}
	return taskID, taskScopeID, nil
}

func resolveTaskExecutionOwnerMemberID(ctx context.Context, queryer coredb.QueryRower, channelID string, createdByRef string) (string, error) {
	trimmedChannelID := strings.TrimSpace(channelID)
	trimmedCreatedByRef := strings.TrimSpace(createdByRef)
	if trimmedCreatedByRef == "" {
		return "", badRequest("createdByRef is required")
	}
	row := queryer.QueryRow(ctx, `
SELECT COALESCE((
    SELECT NULLIF(sm.owner_member_id, '')
    FROM scope_memberships sm
    WHERE sm.scope_id = $1
      AND sm.member_id = $2
      AND sm.visibility = 'private'
      AND sm.owner_member_id IS NOT NULL
      AND sm.effective_to_seq IS NULL
    ORDER BY
      CASE WHEN sm.removed_at IS NULL THEN 0 ELSE 1 END,
      CASE WHEN sm.archived_at IS NULL THEN 0 ELSE 1 END,
      sm.effective_from_seq DESC
    LIMIT 1
), $2)
`, trimmedChannelID, trimmedCreatedByRef)
	var executionOwnerMemberID string
	if err := row.Scan(&executionOwnerMemberID); err != nil {
		return "", fmt.Errorf("resolve task execution owner member: %w", err)
	}
	return strings.TrimSpace(executionOwnerMemberID), nil
}
