import { resolvePoint, resolveRegion } from '../coordinates.js';
import { computerError, isComputerError } from '../errors.js';
import type { CommandRunner } from '../runner.js';
import { SpawnCommandRunner } from '../runner.js';
import type { ChatGptComputerMcpConfig } from '../config.js';
import type { ActionResult, ComputerAction, ComputerInfo, DisplayInfo, PointerButton } from '../types.js';
import type { ComputerBackend } from './computer-backend.js';
import type { Observation, ObserveRequest, Rect } from '../types.js';

const buttonNumber: Record<PointerButton, string> = { left: '1', middle: '2', right: '3' };

function text(buffer: Buffer): string {
  return buffer.toString('utf8');
}

function formatFailure(command: string, stderr: Buffer): string {
  const body = text(stderr).trim();
  return body.length === 0 ? `${command} failed.` : `${command} failed: ${body.slice(0, 500)}`;
}

export function parseXrandrListMonitors(output: string): DisplayInfo[] {
  const displays: DisplayInfo[] = [];
  for (const raw of output.split(/\r?\n/)) {
    const line = raw.trim();
    if (line.length === 0 || line.startsWith('Monitors:')) continue;
    const parts = line.split(/\s+/);
    if (parts.length < 4) continue;
    const descriptor = parts[1];
    const geometry = parts[2];
    const outputName = parts.at(-1);
    if (descriptor === undefined || geometry === undefined || outputName === undefined) continue;
    const match = /^(\d+)\/\d+x(\d+)\/\d+([+-]\d+)([+-]\d+)$/.exec(geometry);
    if (match === null) continue;
    const width = Number(match[1]);
    const height = Number(match[2]);
    const x = Number(match[3]);
    const y = Number(match[4]);
    displays.push({ id: outputName, name: outputName, x, y, width, height, primary: descriptor.includes('*'), pixelWidth: width, pixelHeight: height, scale: 1 });
  }
  return displays;
}

export function virtualBounds(displays: readonly DisplayInfo[]): Rect {
  if (displays.length === 0) throw computerError('BACKEND_UNAVAILABLE', 'computer.info', 'X11 reported no active displays.');
  const minX = Math.min(...displays.map(display => display.x));
  const minY = Math.min(...displays.map(display => display.y));
  const maxX = Math.max(...displays.map(display => display.x + display.width));
  const maxY = Math.max(...displays.map(display => display.y + display.height));
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}

export class X11Backend implements ComputerBackend {
  readonly #display: string;

  constructor(
    readonly config: Readonly<ChatGptComputerMcpConfig>,
    readonly runner: CommandRunner = new SpawnCommandRunner(),
    readonly env: NodeJS.ProcessEnv = process.env,
  ) {
    this.#display = config.x11.display ?? env.DISPLAY ?? '';
    if (this.#display.length === 0) {
      throw computerError('BACKEND_UNAVAILABLE', 'backend.x11', 'X11 backend requires DISPLAY or x11.display configuration.');
    }
  }

  async #runSuccessful(command: string, args: readonly string[], operation: string, maxStdoutBytes = 1024 * 1024): Promise<Buffer> {
    try {
      const result = await this.runner.run(command, args, {
        timeoutMs: 30_000,
        maxStdoutBytes,
        maxStderrBytes: 1024 * 1024,
        env: { DISPLAY: this.#display },
      });
      if (result.timedOut || result.exitCode !== 0) {
        throw computerError('OS_ERROR', operation, formatFailure(command, result.stderr), { exitCode: result.exitCode, timedOut: result.timedOut });
      }
      return result.stdout;
    } catch (error) {
      if (isComputerError(error)) throw error;
      throw computerError('OS_ERROR', operation, `Unexpected ${command} failure.`);
    }
  }

  async info(): Promise<ComputerInfo> {
    const stdout = await this.#runSuccessful('xrandr', ['--listmonitors'], 'computer.info');
    const displays = parseXrandrListMonitors(text(stdout));
    if (displays.length === 0) {
      throw computerError('BACKEND_UNAVAILABLE', 'computer.info', 'Could not parse any active X11 displays from xrandr.');
    }
    return {
      backend: 'x11',
      sessionType: this.env.XDG_SESSION_TYPE ?? 'x11',
      coordinateUnit: 'physical-pixel',
      coordinateSpaces: ['desktop', 'display'],
      virtualBounds: virtualBounds(displays),
      displays,
      capabilities: {
        observe: this.config.observe.enabled,
        pointer: this.config.action.enabled,
        keyboard: this.config.action.enabled,
        text: this.config.action.enabled,
      },
    };
  }

  async observe(request: ObserveRequest): Promise<Observation> {
    if (!this.config.observe.enabled) throw computerError('OBSERVE_DISABLED', 'computer.observe', 'Screen observation is disabled.');
    const info = await this.info();
    const region = resolveRegion(info, request.target);
    if (region.x < 0 || region.y < 0) {
      throw computerError('BACKEND_UNAVAILABLE', 'computer.observe', 'FFmpeg x11grab capture currently requires non-negative X11 framebuffer offsets.', { region });
    }
    const input = `${this.#display}+${region.x},${region.y}`;
    const args = [
      '-hide_banner', '-loglevel', 'error',
      '-f', 'x11grab',
      '-draw_mouse', request.includePointer ? '1' : '0',
      '-video_size', `${region.width}x${region.height}`,
      '-i', input,
      '-frames:v', '1',
      '-f', 'image2pipe', '-vcodec', 'png', 'pipe:1',
    ];
    const data = await this.#runSuccessful('ffmpeg', args, 'computer.observe', this.config.observe.maxImageBytes);
    if (data.byteLength === 0) throw computerError('OS_ERROR', 'computer.observe', 'Screenshot capture returned no image data.');
    if (data.byteLength > this.config.observe.maxImageBytes) {
      throw computerError('OUTPUT_LIMIT', 'computer.observe', 'Screenshot exceeds configured byte limit.', { bytes: data.byteLength, maximum: this.config.observe.maxImageBytes });
    }
    return {
      backend: 'x11',
      mimeType: 'image/png',
      data,
      bytes: data.byteLength,
      capturedAt: new Date().toISOString(),
      region,
      pixelWidth: region.width,
      pixelHeight: region.height,
    };
  }

  async #xdotool(args: readonly string[], operation: string): Promise<void> {
    await this.#runSuccessful('xdotool', args, operation, 256 * 1024);
  }

  async act(action: ComputerAction): Promise<ActionResult> {
    if (!this.config.action.enabled) throw computerError('ACTION_DISABLED', 'computer.act', 'Computer actions are disabled.');
    const info = await this.info();
    const args: string[] = [];
    const addPoint = (point: Parameters<typeof resolvePoint>[1]): void => {
      const resolved = resolvePoint(info, point);
      args.push('mousemove', '--sync', String(resolved.x), String(resolved.y));
    };

    switch (action.type) {
      case 'move':
        addPoint(action.point);
        break;
      case 'click':
        if (action.point !== undefined) addPoint(action.point);
        args.push('click', buttonNumber[action.button]);
        break;
      case 'doubleClick':
        if (action.point !== undefined) addPoint(action.point);
        args.push('click', '--repeat', '2', '--delay', String(action.intervalMs), buttonNumber[action.button]);
        break;
      case 'scroll': {
        if (action.point !== undefined) addPoint(action.point);
        if (!Number.isInteger(action.deltaX) || !Number.isInteger(action.deltaY) || (action.deltaX === 0 && action.deltaY === 0)) {
          throw computerError('INVALID_INPUT', 'computer.act', 'Scroll deltas must be integers and at least one delta must be non-zero.');
        }
        const total = Math.abs(action.deltaX) + Math.abs(action.deltaY);
        if (total > this.config.action.maxScrollSteps) {
          throw computerError('INVALID_INPUT', 'computer.act', 'Scroll exceeds configured maximum steps.', { requested: total, maximum: this.config.action.maxScrollSteps });
        }
        if (action.deltaY !== 0) args.push('click', '--repeat', String(Math.abs(action.deltaY)), action.deltaY > 0 ? '5' : '4');
        if (action.deltaX !== 0) args.push('click', '--repeat', String(Math.abs(action.deltaX)), action.deltaX > 0 ? '7' : '6');
        break;
      }
      case 'drag':
        addPoint(action.from);
        args.push('mousedown', buttonNumber[action.button]);
        addPoint(action.to);
        args.push('mouseup', buttonNumber[action.button]);
        break;
      case 'mouseDown':
        if (action.point !== undefined) addPoint(action.point);
        args.push('mousedown', buttonNumber[action.button]);
        break;
      case 'mouseUp':
        if (action.point !== undefined) addPoint(action.point);
        args.push('mouseup', buttonNumber[action.button]);
        break;
      case 'key':
        if (action.keys.length === 0 || action.keys.length > 32 || action.keys.some(key => key.length === 0 || key.length > 128 || key.includes('\0'))) {
          throw computerError('INVALID_INPUT', 'computer.act', 'keys must contain 1-32 non-empty key/chord names of at most 128 characters each.');
        }
        args.push('key', '--clearmodifiers', ...action.keys);
        break;
      case 'text': {
        if (!Number.isInteger(action.delayMs) || action.delayMs < 0 || action.delayMs > 10_000) {
          throw computerError('INVALID_INPUT', 'computer.act', 'delayMs must be an integer between 0 and 10000.');
        }
        const bytes = Buffer.byteLength(action.text, 'utf8');
        if (bytes > this.config.action.maxTextBytes) {
          throw computerError('INVALID_INPUT', 'computer.act', 'Text exceeds configured byte limit.', { bytes, maximum: this.config.action.maxTextBytes });
        }
        args.push('type', '--clearmodifiers', '--delay', String(action.delayMs), '--', action.text);
        break;
      }
    }

    await this.#xdotool(args, 'computer.act');
    return { type: action.type, executed: true, executedAt: new Date().toISOString() };
  }
}
