import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import type { Sql } from '../db.js';
import { resolveWritePath } from './write_guard.js';
import { locateMatch } from './fuzzy-match.js';

// --- Types -------------------------------------------------------------------

export interface PendingChange {
  id: string;
  session_id: string;
  task_id: string | null;
  file_path: string;
  operation: 'create' | 'edit' | 'delete';
  diff: string;
  status: 'pending' | 'applied' | 'rejected' | 'reverted';
  // v2.6 Phase 1-UX: which agent staged this change (DiffPanel attribution).
  // Native boocode write tools stamp 'boocode'; the manual RightRail create path
  // passes null (renders as "manual"). NULL on legacy rows queued pre-v2.6.
  agent: string | null;
  created_at: string;
}

export interface ApplyResult {
  id: string;
  file_path: string;
  operation: string;
  success: boolean;
  error?: string;
}

// --- Queue functions ---------------------------------------------------------

export async function queueEdit(
  sql: Sql,
  sessionId: string,
  taskId: string | null,
  filePath: string,
  oldString: string,
  newString: string,
  projectRoot: string,
  // v2.6 Phase 1-UX: attribution. Defaults to 'boocode' because the only callers
  // that omit it are the native write tools (edit_file/create_file/delete_file).
  // Pass null explicitly for the manual RightRail create path.
  agent: string | null = 'boocode',
): Promise<PendingChange> {
  const resolved = resolveWritePath(projectRoot, filePath);
  const diff = JSON.stringify({ old: oldString, new: newString });

  const [row] = await sql<PendingChange[]>`
    INSERT INTO pending_changes (session_id, task_id, file_path, operation, diff, agent)
    VALUES (${sessionId}, ${taskId}, ${resolved}, 'edit', ${diff}, ${agent})
    RETURNING *
  `;
  return row!;
}

export async function queueCreate(
  sql: Sql,
  sessionId: string,
  taskId: string | null,
  filePath: string,
  content: string,
  projectRoot: string,
  // See queueEdit: defaults to 'boocode' for the native write tools; the manual
  // RightRail create route passes null.
  agent: string | null = 'boocode',
): Promise<PendingChange> {
  const resolved = resolveWritePath(projectRoot, filePath);

  const [row] = await sql<PendingChange[]>`
    INSERT INTO pending_changes (session_id, task_id, file_path, operation, diff, agent)
    VALUES (${sessionId}, ${taskId}, ${resolved}, 'create', ${content}, ${agent})
    RETURNING *
  `;
  return row!;
}

export async function queueDelete(
  sql: Sql,
  sessionId: string,
  taskId: string | null,
  filePath: string,
  projectRoot: string,
  // See queueEdit: defaults to 'boocode' for the native write tools.
  agent: string | null = 'boocode',
): Promise<PendingChange> {
  const resolved = resolveWritePath(projectRoot, filePath);

  const [row] = await sql<PendingChange[]>`
    INSERT INTO pending_changes (session_id, task_id, file_path, operation, diff, agent)
    VALUES (${sessionId}, ${taskId}, ${resolved}, 'delete', '', ${agent})
    RETURNING *
  `;
  return row!;
}

// --- Apply functions ---------------------------------------------------------

export async function applyOne(
  sql: Sql,
  changeId: string,
  projectRoot: string,
): Promise<ApplyResult> {
  const [change] = await sql<PendingChange[]>`
    SELECT * FROM pending_changes WHERE id = ${changeId} AND status = 'pending'
  `;
  if (!change) {
    return { id: changeId, file_path: '', operation: '', success: false, error: 'change not found or not pending' };
  }

  try {
    // Re-validate path in case projectRoot has shifted
    resolveWritePath(projectRoot, change.file_path);

    switch (change.operation) {
      case 'create': {
        await mkdir(dirname(change.file_path), { recursive: true });
        await writeFile(change.file_path, change.diff, 'utf8');
        break;
      }
      case 'edit': {
        const { old: oldStr, new: newStr } = JSON.parse(change.diff) as { old: string; new: string };
        const content = await readFile(change.file_path, 'utf8');
        const match = locateMatch(content, oldStr);
        if (match.kind === 'ambiguous') {
          throw new Error(
            `old_string matches ${match.count} locations — add surrounding context to disambiguate`,
          );
        }
        if (match.kind === 'not_found') {
          throw new Error(
            'old_string not found in file (even fuzzily) — file may have changed since the edit was queued',
          );
        }
        const updated = content.slice(0, match.start) + newStr + content.slice(match.end);
        await writeFile(change.file_path, updated, 'utf8');
        break;
      }
      case 'delete': {
        // Stash current content in diff for potential rewind
        try {
          const existing = await readFile(change.file_path, 'utf8');
          await sql`UPDATE pending_changes SET diff = ${existing} WHERE id = ${changeId}`;
        } catch {
          // File may already be gone — proceed with status update
        }
        await unlink(change.file_path);
        break;
      }
    }

    await sql`UPDATE pending_changes SET status = 'applied' WHERE id = ${changeId}`;
    return { id: change.id, file_path: change.file_path, operation: change.operation, success: true };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return { id: change.id, file_path: change.file_path, operation: change.operation, success: false, error: message };
  }
}

export async function applyAll(
  sql: Sql,
  sessionId: string,
  projectRoot: string,
): Promise<ApplyResult[]> {
  const pending = await sql<PendingChange[]>`
    SELECT * FROM pending_changes
    WHERE session_id = ${sessionId} AND status = 'pending'
    ORDER BY created_at ASC
  `;
  const results: ApplyResult[] = [];
  for (const change of pending) {
    results.push(await applyOne(sql, change.id, projectRoot));
  }
  return results;
}

// --- Reject functions --------------------------------------------------------

export async function rejectOne(sql: Sql, changeId: string): Promise<void> {
  await sql`UPDATE pending_changes SET status = 'rejected' WHERE id = ${changeId} AND status = 'pending'`;
}

export async function rejectAll(sql: Sql, sessionId: string): Promise<void> {
  await sql`UPDATE pending_changes SET status = 'rejected' WHERE session_id = ${sessionId} AND status = 'pending'`;
}

// --- Rewind functions --------------------------------------------------------

export async function rewindOne(
  sql: Sql,
  changeId: string,
  projectRoot: string,
): Promise<ApplyResult> {
  const [change] = await sql<PendingChange[]>`
    SELECT * FROM pending_changes WHERE id = ${changeId} AND status = 'applied'
  `;
  if (!change) {
    return { id: changeId, file_path: '', operation: '', success: false, error: 'change not found or not applied' };
  }

  try {
    resolveWritePath(projectRoot, change.file_path);

    switch (change.operation) {
      case 'create': {
        // Reverse a create: delete the file
        await unlink(change.file_path);
        break;
      }
      case 'edit': {
        // Reverse an edit: swap old and new
        const { old: oldStr, new: newStr } = JSON.parse(change.diff) as { old: string; new: string };
        const content = await readFile(change.file_path, 'utf8');
        const match = locateMatch(content, newStr);
        if (match.kind === 'ambiguous') {
          throw new Error(
            `new_string matches ${match.count} locations — cannot rewind; add surrounding context to disambiguate`,
          );
        }
        if (match.kind === 'not_found') {
          throw new Error(
            'new_string not found in file (even fuzzily) — cannot rewind; file may have been modified since apply',
          );
        }
        const reverted = content.slice(0, match.start) + oldStr + content.slice(match.end);
        await writeFile(change.file_path, reverted, 'utf8');
        break;
      }
      case 'delete': {
        // Reverse a delete: recreate the file (diff holds the original content stashed at apply time)
        await mkdir(dirname(change.file_path), { recursive: true });
        await writeFile(change.file_path, change.diff, 'utf8');
        break;
      }
    }

    await sql`UPDATE pending_changes SET status = 'reverted' WHERE id = ${changeId}`;
    return { id: change.id, file_path: change.file_path, operation: change.operation, success: true };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return { id: change.id, file_path: change.file_path, operation: change.operation, success: false, error: message };
  }
}

// --- Query functions ---------------------------------------------------------

export async function listPending(sql: Sql, sessionId: string): Promise<PendingChange[]> {
  return sql<PendingChange[]>`
    SELECT * FROM pending_changes
    WHERE session_id = ${sessionId} AND status = 'pending'
    ORDER BY created_at ASC
  `;
}
