// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// tool-requests repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';
import { randomUUID } from 'crypto';

export type ToolRequestCategory = 'requested' | 'blocked' | 'unknown';

export type ToolRequestStatus = 'pending' | 'approved' | 'denied' | 'auto_denied';


/** 現在の movement で使えないツールの要求記録（能動 RequestTool / 受動 block 捕捉）。 */
export interface ToolRequest {
  id: string;
  taskId: string | null;
  jobId: string | null;
  spaceId: string | null;
  pieceName: string;
  movementName: string;
  toolName: string;
  reason: string | null;
  category: ToolRequestCategory;
  status: ToolRequestStatus;
  grantScope: 'task' | 'piece' | null;
  decidedBy: string | null;
  createdAt: string;
  decidedAt: string | null;
}


export interface ToolRequestRow {
  id: string;
  task_id: string | null;
  job_id: string | null;
  space_id: string | null;
  piece_name: string;
  movement_name: string;
  tool_name: string;
  reason: string | null;
  category: string;
  status: string;
  grant_scope: string | null;
  decided_by: string | null;
  created_at: string;
  decided_at: string | null;
}


export function rowToToolRequest(row: ToolRequestRow): ToolRequest {
  return {
    id: row.id,
    taskId: row.task_id ?? null,
    jobId: row.job_id ?? null,
    spaceId: row.space_id ?? null,
    pieceName: row.piece_name,
    movementName: row.movement_name,
    toolName: row.tool_name,
    reason: row.reason ?? null,
    category: (row.category as ToolRequestCategory) ?? 'requested',
    status: (row.status as ToolRequestStatus) ?? 'pending',
    grantScope: (row.grant_scope as 'task' | 'piece' | null) ?? null,
    decidedBy: row.decided_by ?? null,
    createdAt: row.created_at,
    decidedAt: row.decided_at ?? null,
  };
}


/** ピース集計の1行（tool_name×category ごとの件数）。 */
export interface ToolRequestAggregate {
  toolName: string;
  category: ToolRequestCategory;
  count: number;
  lastSeen: string;
  sampleReason: string | null;
}


  /**
   * Record a tool request — either an agent's RequestTool declaration or a
   * passively-captured blocked tool call. De-dups within the same job: an
   * identical still-pending (job_id, movement_name, tool_name) row is reused
   * so a retry loop does not spam the table. Returns the row id.
   */
  export function recordToolRequest(db: Database.Database, params: {
    id?: string;
    taskId?: string | null;
    jobId?: string | null;
    spaceId?: string | null;
    pieceName: string;
    movementName: string;
    toolName: string;
    reason?: string | null;
    category?: ToolRequestCategory;
    status?: ToolRequestStatus;
  }): string {
    const category: ToolRequestCategory = params.category ?? 'requested';
    const status: ToolRequestStatus = params.status ?? 'pending';
    // De-dup pending rows so a retry loop never grows the table without bound
    // (the table has no GC). Scope by the most specific identity available —
    // job → task → piece — so de-dup still works for runs without a job
    // binding. scopeCol is a fixed identifier (not user input) → no injection.
    const scopeCol: 'job_id' | 'task_id' | 'piece_name' = params.jobId
      ? 'job_id'
      : params.taskId
        ? 'task_id'
        : 'piece_name';
    const scopeVal = params.jobId ?? params.taskId ?? params.pieceName;
    const existing = db
      .prepare(
        `SELECT id FROM tool_requests
           WHERE ${scopeCol} = ? AND movement_name = ? AND tool_name = ? AND status = 'pending'
           LIMIT 1`,
      )
      .get(scopeVal, params.movementName, params.toolName) as { id: string } | undefined;
    if (existing) return existing.id;
    const id = params.id ?? randomUUID();
    db
      .prepare(
        `INSERT INTO tool_requests
           (id, task_id, job_id, space_id, piece_name, movement_name, tool_name, reason, category, status)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      )
      .run(
        id,
        params.taskId ?? null,
        params.jobId ?? null,
        params.spaceId ?? null,
        params.pieceName,
        params.movementName,
        params.toolName,
        params.reason ?? null,
        category,
        status,
      );
    return id;
  }


  /** All tool requests for a task, newest first. */
  export function listToolRequestsByTask(db: Database.Database, taskId: string): ToolRequest[] {
    const rows = db
      .prepare(`SELECT * FROM tool_requests WHERE task_id = ? ORDER BY created_at DESC, id DESC`)
      .all(taskId) as ToolRequestRow[];
    return rows.map(rowToToolRequest);
  }


  /** Per-piece aggregation: count + last-seen + a sample reason per
   * (tool_name, category). `sampleReason` is one representative reason (not
   * necessarily the most recent — it is a MAX over the group). */
  export function aggregateToolRequestsByPiece(db: Database.Database, pieceName: string): ToolRequestAggregate[] {
    const rows = db
      .prepare(
        `SELECT tool_name, category, COUNT(*) AS count,
                MAX(created_at) AS last_seen, MAX(reason) AS sample_reason
           FROM tool_requests
          WHERE piece_name = ?
          GROUP BY tool_name, category
          ORDER BY count DESC, tool_name ASC`,
      )
      .all(pieceName) as Array<{
        tool_name: string; category: string; count: number; last_seen: string; sample_reason: string | null;
      }>;
    return rows.map((r) => ({
      toolName: r.tool_name,
      category: (r.category as ToolRequestCategory) ?? 'requested',
      count: r.count,
      lastSeen: r.last_seen,
      sampleReason: r.sample_reason ?? null,
    }));
  }


  /** Fetch a single tool request by id. */
  export function getToolRequest(db: Database.Database, id: string): ToolRequest | null {
    const row = db.prepare(`SELECT * FROM tool_requests WHERE id = ?`).get(id) as ToolRequestRow | undefined;
    return row ? rowToToolRequest(row) : null;
  }


  /**
   * Record an approve/deny/auto-deny decision on a still-pending tool request.
   * Returns false if the row is missing or already decided (idempotent / race
   * safe — a concurrent decide won't double-apply).
   */
  export function decideToolRequest(db: Database.Database, id: string, decision: { status: Exclude<ToolRequestStatus, 'pending'>; grantScope?: 'task' | 'piece' | null; decidedBy?: string | null }): boolean {
    const res = db
      .prepare(
        `UPDATE tool_requests
            SET status = ?, grant_scope = ?, decided_by = ?, decided_at = datetime('now')
          WHERE id = ? AND status = 'pending'`,
      )
      .run(decision.status, decision.grantScope ?? null, decision.decidedBy ?? null, id);
    return res.changes > 0;
  }


  /** Per-task grant overlay (tool names approved inline for this task). */
  export function getGrantedTools(db: Database.Database, taskId: string): string[] {
    const row = db.prepare(`SELECT granted_tools FROM local_tasks WHERE id = ?`).get(taskId) as
      | { granted_tools: string | null }
      | undefined;
    if (!row?.granted_tools) return [];
    try {
      const parsed = JSON.parse(row.granted_tools);
      return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : [];
    } catch {
      return [];
    }
  }


  /** Add a tool to a task's grant overlay (idempotent — de-duped). Returns the new set. */
  export function addGrantedTool(db: Database.Database, taskId: string, toolName: string): string[] {
    const current = getGrantedTools(db, taskId);
    if (current.includes(toolName)) return current;
    const next = [...current, toolName];
    db.prepare(`UPDATE local_tasks SET granted_tools = ? WHERE id = ?`).run(JSON.stringify(next), taskId);
    return next;
  }

// ─── Package requests (RequestPackage → 承認 → install) ──────────────────────

export type PackageRequestStatus = 'pending' | 'approved' | 'denied' | 'auto_denied';

/** エージェント発 Python パッケージ要求（RequestPackage）の記録。 */
export interface PackageRequest {
  id: string;
  taskId: string | null;
  jobId: string | null;
  spaceId: string | null;
  pieceName: string | null;
  movementName: string | null;
  spec: string;
  normalizedName: string;
  reason: string | null;
  status: PackageRequestStatus;
  decidedBy: string | null;
  createdAt: string;
  decidedAt: string | null;
}

export interface PackageRequestRow {
  id: string;
  task_id: string | null;
  job_id: string | null;
  space_id: string | null;
  piece_name: string | null;
  movement_name: string | null;
  spec: string;
  normalized_name: string;
  reason: string | null;
  status: string;
  decided_by: string | null;
  created_at: string;
  decided_at: string | null;
}

export function rowToPackageRequest(row: PackageRequestRow): PackageRequest {
  return {
    id: row.id,
    taskId: row.task_id ?? null,
    jobId: row.job_id ?? null,
    spaceId: row.space_id ?? null,
    pieceName: row.piece_name ?? null,
    movementName: row.movement_name ?? null,
    spec: row.spec,
    normalizedName: row.normalized_name,
    reason: row.reason ?? null,
    status: (row.status as PackageRequestStatus) ?? 'pending',
    decidedBy: row.decided_by ?? null,
    createdAt: row.created_at,
    decidedAt: row.decided_at ?? null,
  };
}

/**
 * Record an agent-declared Python package request (RequestPackage). De-dups
 * within the same job: an identical still-pending (job_id, normalized_name)
 * row is reused so a retry loop does not spam the table. Returns the row id.
 */
export function recordPackageRequest(db: Database.Database, params: {
  id?: string;
  taskId?: string | null;
  jobId?: string | null;
  spaceId?: string | null;
  pieceName?: string | null;
  movementName?: string | null;
  spec: string;
  normalizedName: string;
  reason?: string | null;
  status?: PackageRequestStatus;
}): string {
  const status: PackageRequestStatus = params.status ?? 'pending';
  const scopeCol: 'job_id' | 'task_id' = params.jobId ? 'job_id' : 'task_id';
  const scopeVal = params.jobId ?? params.taskId ?? null;
  if (scopeVal !== null) {
    const existing = db
      .prepare(
        `SELECT id FROM package_requests
           WHERE ${scopeCol} = ? AND normalized_name = ? AND status = 'pending'
           LIMIT 1`,
      )
      .get(scopeVal, params.normalizedName) as { id: string } | undefined;
    if (existing) return existing.id;
  }
  const id = params.id ?? randomUUID();
  db
    .prepare(
      `INSERT INTO package_requests
         (id, task_id, job_id, space_id, piece_name, movement_name, spec, normalized_name, reason, status)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    )
    .run(
      id,
      params.taskId ?? null,
      params.jobId ?? null,
      params.spaceId ?? null,
      params.pieceName ?? null,
      params.movementName ?? null,
      params.spec,
      params.normalizedName,
      params.reason ?? null,
      status,
    );
  return id;
}

/** All package requests for a task, newest first. */
export function listPackageRequestsByTask(db: Database.Database, taskId: string): PackageRequest[] {
  const rows = db
    .prepare(`SELECT * FROM package_requests WHERE task_id = ? ORDER BY created_at DESC, id DESC`)
    .all(taskId) as PackageRequestRow[];
  return rows.map(rowToPackageRequest);
}

/**
 * Normalized names declined (denied OR auto_denied) for a job. RequestPackage
 * uses this on resume to short-circuit a re-request instead of re-parking —
 * otherwise a deny would loop (deny → movement re-runs → re-request → park).
 */
export function listDeclinedPackageNamesByJob(db: Database.Database, jobId: string): string[] {
  const rows = db
    .prepare(
      `SELECT DISTINCT normalized_name FROM package_requests
         WHERE job_id = ? AND status IN ('denied', 'auto_denied')`,
    )
    .all(jobId) as Array<{ normalized_name: string }>;
  return rows.map((r) => r.normalized_name);
}

/** Fetch a single package request by id. */
export function getPackageRequest(db: Database.Database, id: string): PackageRequest | null {
  const row = db.prepare(`SELECT * FROM package_requests WHERE id = ?`).get(id) as PackageRequestRow | undefined;
  return row ? rowToPackageRequest(row) : null;
}

/**
 * Record an approve/deny/auto-deny decision on a still-pending package request.
 * Returns false if the row is missing or already decided (idempotent / race
 * safe — a concurrent decide won't double-apply, so the install path can gate
 * on this to avoid a double install).
 */
export function decidePackageRequest(
  db: Database.Database,
  id: string,
  decision: { status: Exclude<PackageRequestStatus, 'pending'>; decidedBy?: string | null },
): boolean {
  const res = db
    .prepare(
      `UPDATE package_requests
          SET status = ?, decided_by = ?, decided_at = datetime('now')
        WHERE id = ? AND status = 'pending'`,
    )
    .run(decision.status, decision.decidedBy ?? null, id);
  return res.changes > 0;
}

/**
 * Revert an approved request whose install then FAILED back to pending, so it
 * can be retried. CAS on the current 'approved' state (no-op otherwise). The
 * approve path claims (pending→approved) BEFORE installing precisely so a
 * concurrent deny can never leave a denied request with an installed package;
 * on install failure it reverts here.
 */
export function revertPackageRequestToPending(db: Database.Database, id: string): boolean {
  const res = db
    .prepare(
      `UPDATE package_requests
          SET status = 'pending', decided_by = NULL, decided_at = NULL
        WHERE id = ? AND status = 'approved'`,
    )
    .run(id);
  return res.changes > 0;
}
