/**
 * Watch store — sqlite-backed persistence for background "watches".
 *
 * A watch is a long-task poller: a tool flagged `async` registers one,
 * the watch-runner polls it until a terminal state, then feeds the
 * result back into the originating chat session. Persisted (not in
 * memory) so a Forge restart resumes in-flight watches. Lives in
 * <data>/workflow.db alongside sessions.
 *
 * Concurrency: the runner (chat-standalone) is the primary writer;
 * the /api/watches routes (next-server) also write (cancel/delete).
 * better-sqlite3 + WAL serialises writers via the file lock — fine at
 * watch frequencies.
 */

import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { getDb } from '../../src/core/db/database';
import { getDataDir } from '../dirs';

export type WatchState =
  | 'active' | 'done' | 'failed' | 'timed_out' | 'cancelled' | 'errored';

export interface WatchAction {
  mode?: 'chat' | 'tool' | 'none';
  message?: string;
  tool?: string;
  args?: Record<string, unknown>;
}

export interface Watch {
  id: string;
  session_id: string | null;
  label: string;
  connector_id: string;
  poll_tool: string;            // bare tool name (poll), dispatched as `${connector_id}.${poll_tool}`
  poll_args: Record<string, unknown>;
  done_path: string | null;
  done_match: { path: string; equals?: string; contains?: string } | null;
  fail_path: string | null;
  on_done: WatchAction | null;
  on_fail: WatchAction | null;
  progress: { show?: boolean; message?: string } | null;
  interval_sec: number;
  timeout_sec: number;
  max_polls: number;
  chain_depth: number;
  state: WatchState;
  polls: number;
  err_count: number;
  created_at: number;
  next_poll_at: number;
  updated_at: number;
  last_text: string | null;
  last_result: unknown;
}

interface Row {
  id: string; session_id: string | null; label: string; connector_id: string;
  poll_tool: string; poll_args_json: string;
  done_path: string | null; done_match_json: string | null; fail_path: string | null;
  on_done_json: string | null; on_fail_json: string | null; progress_json: string | null;
  interval_sec: number; timeout_sec: number; max_polls: number; chain_depth: number;
  state: string; polls: number; err_count: number;
  created_at: number; next_poll_at: number; updated_at: number;
  last_text: string | null; last_result_json: string | null;
}

let ready = false;
function db() {
  const d = getDb(join(getDataDir(), 'workflow.db'));
  if (!ready) {
    d.exec(`
      CREATE TABLE IF NOT EXISTS connector_watches (
        id TEXT PRIMARY KEY,
        session_id TEXT,
        label TEXT NOT NULL DEFAULT '',
        connector_id TEXT NOT NULL,
        poll_tool TEXT NOT NULL,
        poll_args_json TEXT NOT NULL DEFAULT '{}',
        done_path TEXT,
        done_match_json TEXT,
        fail_path TEXT,
        on_done_json TEXT,
        on_fail_json TEXT,
        progress_json TEXT,
        interval_sec INTEGER NOT NULL DEFAULT 60,
        timeout_sec INTEGER NOT NULL DEFAULT 1200,
        max_polls INTEGER NOT NULL DEFAULT 40,
        chain_depth INTEGER NOT NULL DEFAULT 0,
        state TEXT NOT NULL DEFAULT 'active',
        polls INTEGER NOT NULL DEFAULT 0,
        err_count INTEGER NOT NULL DEFAULT 0,
        created_at INTEGER NOT NULL,
        next_poll_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL,
        last_text TEXT,
        last_result_json TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_watches_state ON connector_watches(state, next_poll_at);
    `);
    ready = true;
  }
  return d;
}

function rowToWatch(r: Row): Watch {
  const j = <T,>(s: string | null, d: T): T => { if (s == null) return d; try { return JSON.parse(s) as T; } catch { return d; } };
  return {
    id: r.id, session_id: r.session_id, label: r.label, connector_id: r.connector_id,
    poll_tool: r.poll_tool, poll_args: j(r.poll_args_json, {}),
    done_path: r.done_path, done_match: j(r.done_match_json, null), fail_path: r.fail_path,
    on_done: j(r.on_done_json, null), on_fail: j(r.on_fail_json, null), progress: j(r.progress_json, null),
    interval_sec: r.interval_sec, timeout_sec: r.timeout_sec, max_polls: r.max_polls, chain_depth: r.chain_depth,
    state: r.state as WatchState, polls: r.polls, err_count: r.err_count,
    created_at: r.created_at, next_poll_at: r.next_poll_at, updated_at: r.updated_at,
    last_text: r.last_text, last_result: j(r.last_result_json, null),
  };
}

export interface NewWatch {
  session_id: string | null;
  label: string;
  connector_id: string;
  poll_tool: string;
  poll_args: Record<string, unknown>;
  done_path?: string | null;
  done_match?: Watch['done_match'];
  fail_path?: string | null;
  on_done?: WatchAction | null;
  on_fail?: WatchAction | null;
  progress?: Watch['progress'];
  interval_sec: number;
  timeout_sec: number;
  max_polls: number;
  chain_depth: number;
  now: number;
}

export function createWatch(w: NewWatch): Watch {
  const id = 'w_' + randomUUID().slice(0, 12);
  const next = w.now + w.interval_sec * 1000;
  db().prepare(`
    INSERT INTO connector_watches
      (id, session_id, label, connector_id, poll_tool, poll_args_json,
       done_path, done_match_json, fail_path, on_done_json, on_fail_json, progress_json,
       interval_sec, timeout_sec, max_polls, chain_depth,
       state, polls, err_count, created_at, next_poll_at, updated_at, last_text, last_result_json)
    VALUES (@id,@session_id,@label,@connector_id,@poll_tool,@poll_args_json,
            @done_path,@done_match_json,@fail_path,@on_done_json,@on_fail_json,@progress_json,
            @interval_sec,@timeout_sec,@max_polls,@chain_depth,
            'active',0,0,@created_at,@next_poll_at,@updated_at,NULL,NULL)
  `).run({
    id, session_id: w.session_id, label: w.label, connector_id: w.connector_id,
    poll_tool: w.poll_tool, poll_args_json: JSON.stringify(w.poll_args || {}),
    done_path: w.done_path ?? null, done_match_json: w.done_match ? JSON.stringify(w.done_match) : null,
    fail_path: w.fail_path ?? null,
    on_done_json: w.on_done ? JSON.stringify(w.on_done) : null,
    on_fail_json: w.on_fail ? JSON.stringify(w.on_fail) : null,
    progress_json: w.progress ? JSON.stringify(w.progress) : null,
    interval_sec: w.interval_sec, timeout_sec: w.timeout_sec, max_polls: w.max_polls, chain_depth: w.chain_depth,
    created_at: w.now, next_poll_at: next, updated_at: w.now,
  });
  return getWatch(id)!;
}

export function getWatch(id: string): Watch | null {
  const r = db().prepare(`SELECT * FROM connector_watches WHERE id = ?`).get(id) as Row | undefined;
  return r ? rowToWatch(r) : null;
}

export function countActive(): number {
  const r = db().prepare(`SELECT COUNT(*) AS n FROM connector_watches WHERE state = 'active'`).get() as { n: number };
  return r.n;
}

/** Active watches whose next poll is due (<= now). */
export function listDue(now: number): Watch[] {
  const rows = db().prepare(
    `SELECT * FROM connector_watches WHERE state = 'active' AND next_poll_at <= ? ORDER BY next_poll_at ASC`,
  ).all(now) as Row[];
  return rows.map(rowToWatch);
}

/** All watches (active first, then recent terminal), for the management UI. */
export function listWatches(limit = 50): Watch[] {
  const rows = db().prepare(
    `SELECT * FROM connector_watches ORDER BY (state='active') DESC, updated_at DESC LIMIT ?`,
  ).all(limit) as Row[];
  return rows.map(rowToWatch);
}

export function updateWatch(id: string, patch: Partial<{
  state: WatchState; polls: number; err_count: number; next_poll_at: number;
  last_text: string | null; last_result: unknown;
}>, now: number): void {
  const sets: string[] = ['updated_at = @updated_at'];
  const params: Record<string, unknown> = { id, updated_at: now };
  if (patch.state !== undefined) { sets.push('state = @state'); params.state = patch.state; }
  if (patch.polls !== undefined) { sets.push('polls = @polls'); params.polls = patch.polls; }
  if (patch.err_count !== undefined) { sets.push('err_count = @err_count'); params.err_count = patch.err_count; }
  if (patch.next_poll_at !== undefined) { sets.push('next_poll_at = @next_poll_at'); params.next_poll_at = patch.next_poll_at; }
  if (patch.last_text !== undefined) { sets.push('last_text = @last_text'); params.last_text = patch.last_text; }
  if (patch.last_result !== undefined) { sets.push('last_result_json = @last_result_json'); params.last_result_json = patch.last_result == null ? null : JSON.stringify(patch.last_result); }
  db().prepare(`UPDATE connector_watches SET ${sets.join(', ')} WHERE id = @id`).run(params);
}

/** Cancel an active watch (terminal). Returns false if not found/active. */
export function cancelWatch(id: string, now: number): boolean {
  const r = db().prepare(
    `UPDATE connector_watches SET state='cancelled', updated_at=? WHERE id=? AND state='active'`,
  ).run(now, id);
  return r.changes > 0;
}

export function deleteWatch(id: string): boolean {
  return db().prepare(`DELETE FROM connector_watches WHERE id = ?`).run(id).changes > 0;
}
