import type { InteractiveProcess, InteractiveSpawner } from '../../execution/index.ts';
import { getDefaultInteractiveSpawner } from '../../execution/index.ts';
import type { DebuggerBackend } from './DebuggerBackend.ts';
import type { BreakpointInfo, BreakpointSpec, DebugExecutionState } from '../types.ts';

const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
const LLDB_PROMPT = 'XCODEBUILDMCP_LLDB> ';
const COMMAND_SENTINEL = '__XCODEBUILDMCP_DONE__';
const COMMAND_SENTINEL_REGEX = new RegExp(`(^|\\r?\\n)${COMMAND_SENTINEL}(\\r?\\n)`);

class LldbCliBackend implements DebuggerBackend {
  readonly kind = 'lldb-cli' as const;

  private readonly spawner: InteractiveSpawner;
  private readonly prompt = LLDB_PROMPT;
  private readonly process: InteractiveProcess;
  private buffer = '';
  private pending: {
    resolve: (output: string) => void;
    reject: (error: Error) => void;
    timeout: NodeJS.Timeout;
  } | null = null;
  private queue: Promise<unknown> = Promise.resolve();
  private ready: Promise<void>;
  private disposed = false;

  constructor(spawner: InteractiveSpawner) {
    this.spawner = spawner;
    const lldbCommand = [
      'xcrun',
      'lldb',
      '--no-lldbinit',
      '-o',
      `settings set prompt "${this.prompt}"`,
    ];

    this.process = this.spawner(lldbCommand);

    this.process.process.stdout?.on('data', (data: Buffer) => this.handleData(data));
    this.process.process.stderr?.on('data', (data: Buffer) => this.handleData(data));
    this.process.process.on('exit', (code, signal) => {
      const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
      this.failPending(new Error(`LLDB process exited (${detail})`));
    });

    this.ready = this.initialize();
  }

  private async initialize(): Promise<void> {
    // Prime the prompt by running a sentinel command we can parse reliably.
    this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
    await this.waitForSentinel(DEFAULT_STARTUP_TIMEOUT_MS);
  }

  async waitUntilReady(): Promise<void> {
    await this.ready;
  }

  async attach(opts: { pid: number; simulatorId: string; waitFor?: boolean }): Promise<void> {
    const command = opts.waitFor
      ? `process attach --pid ${opts.pid} --waitfor`
      : `process attach --pid ${opts.pid}`;
    const output = await this.runCommand(command);
    assertNoLldbError('attach', output);
  }

  async detach(): Promise<void> {
    const output = await this.runCommand('process detach');
    assertNoLldbError('detach', output);
  }

  async runCommand(command: string, opts?: { timeoutMs?: number }): Promise<string> {
    return this.enqueue(async () => {
      if (this.disposed) {
        throw new Error('LLDB backend disposed');
      }
      await this.ready;
      this.process.write(`${command}\n`);
      this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
      const output = await this.waitForSentinel(opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
      return sanitizeOutput(output, this.prompt).trimEnd();
    });
  }

  async resume(): Promise<void> {
    return this.enqueue(async () => {
      if (this.disposed) {
        throw new Error('LLDB backend disposed');
      }
      await this.ready;
      this.process.write('process continue\n');
    });
  }

  async addBreakpoint(
    spec: BreakpointSpec,
    opts?: { condition?: string },
  ): Promise<BreakpointInfo> {
    const command =
      spec.kind === 'file-line'
        ? `breakpoint set --file "${spec.file}" --line ${spec.line}`
        : `breakpoint set --name "${spec.name}"`;
    const output = await this.runCommand(command);
    assertNoLldbError('breakpoint', output);

    const match = output.match(/Breakpoint\s+(\d+):/);
    if (!match) {
      throw new Error(`Unable to parse breakpoint id from output: ${output}`);
    }

    const id = Number(match[1]);

    if (opts?.condition) {
      const condition = formatConditionForLldb(opts.condition);
      const modifyOutput = await this.runCommand(`breakpoint modify -c ${condition} ${id}`);
      assertNoLldbError('breakpoint modify', modifyOutput);
    }

    return {
      id,
      spec,
      rawOutput: output,
    };
  }

  async removeBreakpoint(id: number): Promise<string> {
    const output = await this.runCommand(`breakpoint delete ${id}`);
    assertNoLldbError('breakpoint delete', output);
    return output;
  }

  async getStack(opts?: { threadIndex?: number; maxFrames?: number }): Promise<string> {
    let command = 'thread backtrace';
    if (typeof opts?.maxFrames === 'number') {
      command += ` -c ${opts.maxFrames}`;
    }
    if (typeof opts?.threadIndex === 'number') {
      command += ` ${opts.threadIndex}`;
    }
    return this.runCommand(command);
  }

  async getVariables(opts?: { frameIndex?: number }): Promise<string> {
    if (typeof opts?.frameIndex === 'number') {
      await this.runCommand(`frame select ${opts.frameIndex}`);
    }
    return this.runCommand('frame variable');
  }

  async getExecutionState(opts?: { timeoutMs?: number }): Promise<DebugExecutionState> {
    try {
      const output = await this.runCommand('process status', {
        timeoutMs: opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
      });
      const normalized = output.toLowerCase();

      if (/no process|exited|terminated/.test(normalized)) {
        return { status: 'terminated', description: output.trim() };
      }
      if (/\bstopped\b/.test(normalized)) {
        return {
          status: 'stopped',
          reason: parseStopReason(output),
          description: output.trim(),
        };
      }
      if (/\brunning\b/.test(normalized)) {
        return { status: 'running', description: output.trim() };
      }
      if (/error:/.test(normalized)) {
        return { status: 'unknown', description: output.trim() };
      }

      return { status: 'unknown', description: output.trim() };
    } catch (error) {
      return {
        status: 'unknown',
        description: error instanceof Error ? error.message : String(error),
      };
    }
  }

  async dispose(): Promise<void> {
    if (this.disposed) return;
    this.disposed = true;
    this.failPending(new Error('LLDB backend disposed'));
    this.process.dispose();
  }

  private enqueue<T>(work: () => Promise<T>): Promise<T> {
    const next = this.queue.then(work, work) as Promise<T>;
    this.queue = next.then(
      () => undefined,
      () => undefined,
    );
    return next;
  }

  private handleData(data: Buffer): void {
    this.buffer += data.toString('utf8');
    this.checkPending();
  }

  private waitForSentinel(timeoutMs: number): Promise<string> {
    if (this.pending) {
      return Promise.reject(new Error('LLDB command already pending'));
    }

    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pending = null;
        reject(new Error(`LLDB command timed out after ${timeoutMs}ms`));
      }, timeoutMs);

      this.pending = { resolve, reject, timeout };
      this.checkPending();
    });
  }

  private checkPending(): void {
    if (!this.pending) return;
    const sentinelMatch = this.buffer.match(COMMAND_SENTINEL_REGEX);
    const sentinelIndex = sentinelMatch?.index;
    const sentinelLength = sentinelMatch?.[0].length;
    if (sentinelIndex == null || sentinelLength == null) return;

    const output = this.buffer.slice(0, sentinelIndex);
    const remainderStart = sentinelIndex + sentinelLength;

    const promptIndex = this.buffer.indexOf(this.prompt, remainderStart);
    if (promptIndex !== -1) {
      this.buffer = this.buffer.slice(promptIndex + this.prompt.length);
    } else {
      this.buffer = this.buffer.slice(remainderStart);
    }

    const { resolve, timeout } = this.pending;
    this.pending = null;
    clearTimeout(timeout);
    resolve(output);
  }

  private failPending(error: Error): void {
    if (!this.pending) return;
    const { reject, timeout } = this.pending;
    this.pending = null;
    clearTimeout(timeout);
    reject(error);
  }
}

function assertNoLldbError(context: string, output: string): void {
  if (/error:/i.test(output)) {
    throw new Error(`LLDB ${context} failed: ${output.trim()}`);
  }
}

function sanitizeOutput(output: string, prompt: string): string {
  const lines = output.split(/\r?\n/);
  const filtered = lines.filter((line) => {
    if (!line) return false;
    if (line.startsWith(prompt)) return false;
    if (line.includes(`script print("${COMMAND_SENTINEL}")`)) return false;
    if (line.includes(COMMAND_SENTINEL)) return false;
    return true;
  });
  return filtered.join('\n');
}

function formatConditionForLldb(condition: string): string {
  const escaped = condition.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  return `"${escaped}"`;
}

function parseStopReason(output: string): string | undefined {
  const match = output.match(/stop reason\s*=\s*(.+)/i);
  if (!match) return undefined;
  return match[1]?.trim() || undefined;
}

export async function createLldbCliBackend(
  spawner: InteractiveSpawner = getDefaultInteractiveSpawner(),
): Promise<DebuggerBackend> {
  const backend = new LldbCliBackend(spawner);
  try {
    await backend.waitUntilReady();
  } catch (error) {
    try {
      await backend.dispose();
    } catch {
      // Best-effort cleanup; keep original error.
    }
    throw error;
  }
  return backend;
}
