import type { Database } from 'bun:sqlite';
import {
  DB_EVIDENCE_ACTIONS,
  isDbEvidenceAction,
  type DbEvidenceAction,
} from 'ejclaw-runners-shared';

import { parseGitHubCiMetadata } from './github-ci.js';
import { extractWatchCiTarget } from './task-watch-status.js';

type SqlBinding = string | number | bigint | boolean | null | Uint8Array;

export { DB_EVIDENCE_ACTIONS, isDbEvidenceAction, type DbEvidenceAction };

export interface DbEvidenceRequest {
  action: DbEvidenceAction;
  taskId?: string;
  minutes?: number;
  limit?: number;
}

export interface DbEvidenceScope {
  sourceGroup: string;
  isMain: boolean;
}

const DEFAULT_RECENT_MINUTES = 60;
const MAX_RECENT_MINUTES = 24 * 60;
const DEFAULT_ROW_LIMIT = 20;
const MAX_ROW_LIMIT = 100;
const TASK_ID_PATTERN = /^[A-Za-z0-9._:@/-]{1,200}$/;

export function normalizeDbEvidenceMinutes(value?: number): number {
  if (!Number.isFinite(value)) {
    return DEFAULT_RECENT_MINUTES;
  }
  const normalized = Math.trunc(value as number);
  return Math.min(Math.max(normalized, 1), MAX_RECENT_MINUTES);
}

export function normalizeDbEvidenceLimit(value?: number): number {
  if (!Number.isFinite(value)) {
    return DEFAULT_ROW_LIMIT;
  }
  const normalized = Math.trunc(value as number);
  return Math.min(Math.max(normalized, 1), MAX_ROW_LIMIT);
}

export function normalizeDbEvidenceTaskId(value?: string): string {
  const taskId = value?.trim();
  if (!taskId || !TASK_ID_PATTERN.test(taskId)) {
    throw new Error(`Unsupported paired task id for DB evidence: ${value}`);
  }
  return taskId;
}

function stringifyEvidence(value: unknown): string {
  return JSON.stringify(value, null, 2);
}

function groupScopeClause(scope: DbEvidenceScope): {
  clause: string;
  params: SqlBinding[];
} {
  return scope.isMain
    ? { clause: '', params: [] }
    : { clause: ' AND group_folder = ?', params: [scope.sourceGroup] };
}

function getScopedTask(
  database: Database,
  taskId: string,
  scope: DbEvidenceScope,
): Record<string, unknown> | null {
  const groupScope = groupScopeClause(scope);
  const row = database
    .prepare(
      `
          SELECT id,
                 group_folder,
                 chat_jid,
                 status,
                 round_trip_count,
                 owner_failure_count,
                 owner_step_done_streak,
                 finalize_step_done_count,
                 task_done_then_user_reopen_count,
                 empty_step_done_streak,
                 arbiter_verdict,
                 arbiter_requested_at,
                 completion_reason,
                 owner_agent_type,
                 reviewer_agent_type,
                 arbiter_agent_type,
                 created_at,
                 updated_at
            FROM paired_tasks
           WHERE id = ?
                 ${groupScope.clause}
        `,
    )
    .get(taskId, ...groupScope.params) as Record<string, unknown> | undefined;
  return row ?? null;
}

function runPairedTaskStatus(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  const taskId = normalizeDbEvidenceTaskId(request.taskId);
  const task = getScopedTask(database, taskId, scope);
  return stringifyEvidence({
    action: request.action,
    task,
  });
}

function runPairedTaskFlow(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  const taskId = normalizeDbEvidenceTaskId(request.taskId);
  const limit = normalizeDbEvidenceLimit(request.limit);
  const task = getScopedTask(database, taskId, scope);
  if (!task) {
    return stringifyEvidence({
      action: request.action,
      task: null,
      turns: [],
      attempts: [],
      outputs: [],
      deliveries: [],
    });
  }

  const turns = database
    .prepare(
      `
        SELECT turn_id,
               role,
               intent_kind,
               created_at,
               updated_at
          FROM paired_turns
         WHERE task_id = ?
         ORDER BY updated_at, created_at, turn_id
         LIMIT ?
      `,
    )
    .all(taskId, limit);

  const attempts = database
    .prepare(
      `
        SELECT attempt_id,
               parent_attempt_id,
               turn_id,
               attempt_no,
               role,
               intent_kind,
               state,
               executor_agent_type,
               active_run_id,
               created_at,
               updated_at,
               completed_at,
               length(last_error) AS last_error_chars
          FROM paired_turn_attempts
         WHERE task_id = ?
         ORDER BY updated_at, attempt_no, attempt_id
         LIMIT ?
      `,
    )
    .all(taskId, limit);

  const outputs = database
    .prepare(
      `
        SELECT turn_number,
               role,
               verdict,
               created_at,
               length(output_text) AS output_chars
          FROM paired_turn_outputs
         WHERE task_id = ?
         ORDER BY turn_number, created_at, role
         LIMIT ?
      `,
    )
    .all(taskId, limit);

  const deliveries = database
    .prepare(
      `
        SELECT id,
               agent_type,
               delivery_role,
               status,
               delivery_attempts,
               created_at,
               updated_at,
               delivered_at,
               length(result_payload) AS result_payload_chars,
               length(last_error) AS last_error_chars
          FROM work_items
         WHERE chat_jid = ?
           AND created_at >= ?
           AND created_at <= datetime(?, '+1 minute')
         ORDER BY created_at, id
         LIMIT ?
      `,
    )
    .all(
      String(task.chat_jid),
      String(task.created_at),
      String(task.updated_at),
      limit,
    );

  return stringifyEvidence({
    action: request.action,
    task,
    turns,
    attempts,
    outputs,
    deliveries,
  });
}

function runRecentPairedFailures(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  const minutes = normalizeDbEvidenceMinutes(request.minutes);
  const limit = normalizeDbEvidenceLimit(request.limit);
  const cutoff = new Date(Date.now() - minutes * 60_000).toISOString();
  const groupScope = groupScopeClause(scope);

  const tasks = database
    .prepare(
      `
        SELECT id,
               group_folder,
               chat_jid,
               status,
               owner_failure_count,
               arbiter_verdict,
               completion_reason,
               arbiter_requested_at,
               updated_at
          FROM paired_tasks
         WHERE updated_at >= ?
           AND (
             owner_failure_count > 0
             OR status = 'arbiter_requested'
             OR arbiter_verdict IS NOT NULL
             OR completion_reason IS NOT NULL
           )
           ${groupScope.clause}
         ORDER BY updated_at DESC
         LIMIT ?
      `,
    )
    .all(cutoff, ...groupScope.params, limit);

  const attempts = database
    .prepare(
      `
        SELECT attempts.task_id,
               attempts.turn_id,
               attempts.attempt_no,
               attempts.role,
               attempts.intent_kind,
               attempts.state,
               attempts.executor_agent_type,
               attempts.active_run_id,
               attempts.updated_at,
               attempts.completed_at,
               length(attempts.last_error) AS last_error_chars
          FROM paired_turn_attempts attempts
          JOIN paired_tasks tasks
            ON tasks.id = attempts.task_id
         WHERE attempts.updated_at >= ?
           AND (
             attempts.state = 'failed'
             OR attempts.last_error IS NOT NULL
           )
           ${scope.isMain ? '' : 'AND tasks.group_folder = ?'}
         ORDER BY attempts.updated_at DESC
         LIMIT ?
      `,
    )
    .all(cutoff, ...(scope.isMain ? [] : [scope.sourceGroup]), limit);

  const deliveryRetries = database
    .prepare(
      `
        SELECT id,
               chat_jid,
               agent_type,
               delivery_role,
               status,
               delivery_attempts,
               updated_at,
               length(last_error) AS last_error_chars
          FROM work_items
         WHERE updated_at >= ?
           AND status = 'delivery_retry'
           ${scope.isMain ? '' : 'AND group_folder = ?'}
         ORDER BY updated_at DESC
         LIMIT ?
      `,
    )
    .all(cutoff, ...(scope.isMain ? [] : [scope.sourceGroup]), limit);

  return stringifyEvidence({
    action: request.action,
    window_minutes: minutes,
    cutoff,
    tasks,
    attempts,
    delivery_retries: deliveryRetries,
  });
}

function normalizeScheduledTaskEvidenceRow(
  row: Record<string, unknown>,
): Record<string, unknown> {
  const prompt = typeof row.prompt === 'string' ? row.prompt : '';
  const metadata = parseGitHubCiMetadata(
    typeof row.ci_metadata === 'string' ? row.ci_metadata : null,
  );

  return {
    id: row.id,
    group_folder: row.group_folder,
    chat_jid: row.chat_jid,
    agent_type: row.agent_type,
    room_role: row.room_role,
    ci_provider: row.ci_provider,
    ci_repo: metadata?.repo ?? null,
    ci_run_id: metadata?.run_id ?? null,
    ci_poll_count: metadata?.poll_count ?? null,
    ci_consecutive_errors: metadata?.consecutive_errors ?? null,
    ci_last_checked_at: metadata?.last_checked_at ?? null,
    ci_target: extractWatchCiTarget(prompt),
    max_duration_ms: row.max_duration_ms,
    status_message_id: row.status_message_id,
    status_started_at: row.status_started_at,
    schedule_type: row.schedule_type,
    schedule_value: row.schedule_value,
    next_run: row.next_run,
    last_run: row.last_run,
    last_result_chars: row.last_result_chars,
    status: row.status,
    created_at: row.created_at,
    prompt_chars: row.prompt_chars,
  };
}

function runRecentScheduledTasks(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  const minutes = normalizeDbEvidenceMinutes(request.minutes);
  const limit = normalizeDbEvidenceLimit(request.limit);
  const cutoff = new Date(Date.now() - minutes * 60_000).toISOString();
  const groupScope = groupScopeClause(scope);

  const rows = database
    .prepare(
      `
        SELECT id,
               group_folder,
               chat_jid,
               agent_type,
               room_role,
               ci_provider,
               ci_metadata,
               max_duration_ms,
               status_message_id,
               status_started_at,
               prompt,
               length(prompt) AS prompt_chars,
               schedule_type,
               schedule_value,
               next_run,
               last_run,
               length(last_result) AS last_result_chars,
               status,
               created_at
          FROM scheduled_tasks
         WHERE (
             created_at >= ?
             OR last_run >= ?
             OR next_run >= ?
             OR status_started_at >= ?
             OR status IN ('active', 'paused')
           )
           ${groupScope.clause}
         ORDER BY COALESCE(last_run, status_started_at, next_run, created_at) DESC
         LIMIT ?
      `,
    )
    .all(cutoff, cutoff, cutoff, cutoff, ...groupScope.params, limit) as Array<
    Record<string, unknown>
  >;

  return stringifyEvidence({
    action: request.action,
    window_minutes: minutes,
    cutoff,
    tasks: rows.map(normalizeScheduledTaskEvidenceRow),
  });
}

function runScheduledTaskRuns(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  const minutes = normalizeDbEvidenceMinutes(request.minutes);
  const limit = normalizeDbEvidenceLimit(request.limit);
  const cutoff = new Date(Date.now() - minutes * 60_000).toISOString();
  const taskId = request.taskId
    ? normalizeDbEvidenceTaskId(request.taskId)
    : null;
  const params: SqlBinding[] = [cutoff];
  const clauses = ['logs.run_at >= ?'];

  if (taskId) {
    clauses.push('logs.task_id = ?');
    params.push(taskId);
  }
  if (!scope.isMain) {
    clauses.push('tasks.group_folder = ?');
    params.push(scope.sourceGroup);
  }
  params.push(limit);

  const rows = database
    .prepare(
      `
        SELECT logs.task_id,
               tasks.group_folder,
               tasks.chat_jid,
               tasks.agent_type,
               tasks.room_role,
               tasks.ci_provider,
               logs.run_at,
               logs.duration_ms,
               logs.status,
               length(logs.result) AS result_chars,
               length(logs.error) AS error_chars
          FROM task_run_logs logs
          LEFT JOIN scheduled_tasks tasks
            ON tasks.id = logs.task_id
         WHERE ${clauses.join(' AND ')}
         ORDER BY logs.run_at DESC, logs.id DESC
         LIMIT ?
      `,
    )
    .all(...params);

  return stringifyEvidence({
    action: request.action,
    window_minutes: minutes,
    cutoff,
    task_id: taskId,
    runs: rows,
  });
}

export function runDbEvidenceRequest(
  database: Database,
  request: DbEvidenceRequest,
  scope: DbEvidenceScope,
): string {
  switch (request.action) {
    case 'db_paired_task_status':
      return runPairedTaskStatus(database, request, scope);
    case 'db_paired_task_flow':
      return runPairedTaskFlow(database, request, scope);
    case 'db_recent_paired_failures':
      return runRecentPairedFailures(database, request, scope);
    case 'db_recent_scheduled_tasks':
      return runRecentScheduledTasks(database, request, scope);
    case 'db_scheduled_task_runs':
      return runScheduledTaskRuns(database, request, scope);
  }
}
