import path from 'path';
import os from 'os';
import { promisify } from 'util';
import { ChildProcess, execFile, spawn, type ExecFileException } from 'child_process';
import fs from 'fs';
import { setTimeout as delay } from 'timers/promises';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { CursorCliAgentRequirement } from '../types';
import { AiAgentController, AgentPreparationResult } from './types';
import {
  CursorConfigSnapshot,
  ensureCursorConfig,
  expandHome,
  restoreCursorConfig,
  waitForCursorConnection,
} from './cursorShared';

const execFileAsync = promisify(execFile);

const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.cursor', 'mcp.json');
const DEFAULT_SERVER_KEY = 'mcpx';
const DEFAULT_URL = 'http://127.0.0.1:9000/mcp';
const DEFAULT_TIMEOUT_SEC = 120;
const DEFAULT_COMMAND = 'cursor-agent';
const DEFAULT_LAUNCH_ARGS = ['agent'];
const DEFAULT_INSTALL_SCRIPT_URL = 'https://cursor.com/install';

interface CursorCliState {
  configPath: string;
  projectConfigPath: string;
  serverKey: string;
  url: string;
  startupTimeoutSec: number;
  command: string;
  launchArgs: string[];
  installIfMissing: boolean;
  installScriptUrl: string;
  autoLogin: boolean;
  loginArgs?: string[];
  resolvedCommand?: string;
  useStub: boolean;
  allowStubFallback: boolean;
}

export class CursorCliController implements AiAgentController {
  public readonly requirement: CursorCliAgentRequirement;
  private readonly state: CursorCliState;
  private configSnapshot?: CursorConfigSnapshot;
  private child?: ChildProcess;
  private agentReady = false;
  private useStub: boolean;
  private stubClient?: Client;
  private stubTransport?: SSEClientTransport;

  constructor(requirement: CursorCliAgentRequirement, state: CursorCliState) {
    this.requirement = requirement;
    this.state = state;
    this.useStub = state.useStub;
  }

  async prepare(): Promise<AgentPreparationResult> {
    if (!isSupportedPlatform()) {
      if (this.requirement.skipIfMissing ?? true) {
        console.warn('⚠️  Cursor CLI tests require macOS or Linux. Skipping scenario.');
        return 'skip';
      }
      throw new Error('Cursor CLI tests require macOS or Linux.');
    }

    if (!this.useStub) {
      const resolved = await this.ensureCommand();
      if (!resolved) {
        if (this.state.allowStubFallback) {
          console.warn('⚠️  Cursor Agent CLI not available. Falling back to stub implementation.');
          this.useStub = true;
        } else if (this.requirement.skipIfMissing ?? true) {
          console.warn('⚠️  Cursor Agent CLI not available. Skipping scenario.');
          return 'skip';
        } else {
          throw new Error('Cursor Agent CLI executable not found and auto-install disabled.');
        }
      } else {
        this.state.resolvedCommand = resolved;
      }
    }

    if (this.useStub) {
      console.log('→ Using Cursor Agent CLI stub implementation');
    }

    this.configSnapshot = await ensureCursorConfig({
      configPath: this.state.configPath,
      projectConfigPath: this.state.projectConfigPath,
      serverKey: this.state.serverKey,
      url: this.state.url,
    });

    return 'ready';
  }

  async start(): Promise<void> {
    this.agentReady = false;

    if (this.useStub) {
      await this.startStub();
      return;
    }

    const command = this.state.resolvedCommand ?? this.state.command;
    console.log(`→ Launching Cursor Agent CLI: ${command} ${this.state.launchArgs.join(' ')}`);

    if (this.state.autoLogin) {
      await this.tryLogin(command);
      if (this.useStub) {
        await this.startStub();
        return;
      }
    }

    const child = spawn(command, this.state.launchArgs, {
      env: this.buildEnv(),
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    this.child = child;

    child.stdout?.on('data', (chunk) => {
      process.stdout.write(`[cursor-agent] ${chunk}`);
    });
    child.stderr?.on('data', (chunk) => {
      process.stderr.write(`[cursor-agent] ${chunk}`);
    });

    let exitHandler: ((code: number | null, signal: NodeJS.Signals | null) => void) | undefined;
    let errorHandler: ((err: Error) => void) | undefined;
    const exitPromise = new Promise<never>((_, reject) => {
      const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
        if (!this.agentReady) {
          reject(
            new Error(
              `Cursor Agent CLI exited before connecting (code: ${code ?? 'null'}, signal: ${
                signal ?? 'null'
              })`
            )
          );
        }
      };
      const onError = (err: Error) => {
        if (!this.agentReady) {
          reject(err);
        }
      };

      exitHandler = onExit;
      errorHandler = onError;

      child.once('exit', onExit);
      child.once('error', onError);
    });

    try {
      await Promise.race([
        waitForCursorConnection({ startupTimeoutSec: this.state.startupTimeoutSec }),
        exitPromise,
      ]);
      this.agentReady = true;
    } finally {
      if (exitHandler) {
        child.off('exit', exitHandler);
      }
      if (errorHandler) {
        child.off('error', errorHandler);
      }
    }
  }

  async cleanup(): Promise<void> {
    if (this.useStub) {
      await this.stopStub();
    } else if (this.child) {
      try {
        console.log('→ Stopping Cursor Agent CLI');
        const child = this.child;
        this.child = undefined;
        child.kill('SIGINT');
        const exited = new Promise<void>((resolve) => {
          child.once('exit', () => resolve());
        });
        await Promise.race([exited, delay(5_000)]);
        if (child.exitCode === null && child.signalCode === null) {
          child.kill('SIGKILL');
        }
      } catch (err) {
        console.warn('⚠️  Failed to stop Cursor Agent CLI:', (err as Error).message);
      }
    }

    if (this.configSnapshot) {
      await restoreCursorConfig(this.configSnapshot);
    }
  }

  private async startStub(): Promise<void> {
    console.log('→ Launching Cursor Agent CLI stub');

    const headers: Record<string, string> = {
      'x-lunar-consumer-tag': 'Cursor',
      'user-agent': 'cursor-agent-stub/1.0.0',
    };

    const baseUrl = new URL(this.state.url);
    const sseUrl = new URL(baseUrl.toString());
    sseUrl.pathname = '/sse';

    const transport = new SSEClientTransport(sseUrl, {
      eventSourceInit: {
        fetch: async (url, init) => {
          const combined = new Headers(init?.headers);
          for (const [key, value] of Object.entries(headers)) {
            combined.set(key, value);
          }
          return fetch(url, { ...init, headers: combined });
        },
      },
      requestInit: { headers },
    });
    transport.onerror = (error) => {
      console.warn('⚠️  Cursor Agent CLI stub transport error:', error.message ?? error);
    };
    this.stubTransport = transport;

    const client = new Client({ name: 'Cursor CLI (Stub)', version: '1.0.0' });
    this.stubClient = client;

    try {
      await client.connect(transport);
      await waitForCursorConnection({ startupTimeoutSec: this.state.startupTimeoutSec });
      this.agentReady = true;
    } catch (err) {
      await this.stopStub();
      throw err;
    }
  }

  private async stopStub(): Promise<void> {
    const client = this.stubClient;
    const transport = this.stubTransport;
    this.stubClient = undefined;
    this.stubTransport = undefined;

    if (!client && !transport) {
      return;
    }

    console.log('→ Stopping Cursor Agent CLI stub');

    try {
      await client?.close();
    } catch (err) {
      console.warn('⚠️  Failed to close Cursor Agent CLI stub client:', (err as Error).message);
    }

    try {
      await transport?.close();
    } catch (err) {
      console.warn('⚠️  Failed to close Cursor Agent CLI stub transport:', (err as Error).message);
    }
  }

  private async ensureCommand(): Promise<string | undefined> {
    const candidates = this.getCommandCandidates();

    for (const candidate of candidates) {
      const resolved = await this.tryResolveCommand(candidate);
      if (resolved) return resolved;
    }

    if (!this.state.installIfMissing) {
      return undefined;
    }

    console.log('→ Cursor Agent CLI not found. Attempting installation...');
    await this.installCursorCli();

    for (const candidate of candidates) {
      const resolved = await this.tryResolveCommand(candidate);
      if (resolved) return resolved;
    }

    return undefined;
  }

  private getCommandCandidates(): string[] {
    const { command } = this.state;
    const expanded = expandHome(command);
    const candidates = [command];
    if (expanded !== command) {
      candidates.push(expanded);
    }

    const localBin = path.join(os.homedir(), '.local', 'bin', command);
    if (!candidates.includes(localBin)) {
      candidates.push(localBin);
    }

    return candidates;
  }

  private async tryResolveCommand(candidate: string): Promise<string | undefined> {
    try {
      await execFileAsync(candidate, ['--version'], {
        env: this.buildEnv(),
        timeout: 10_000,
      });
      return candidate;
    } catch (err: any) {
      if (err?.code === 'ENOENT') {
        return undefined;
      }
      // Some paths might exist but not be executable; fall back to fs check for better logging.
      if (await fileExists(candidate)) {
        console.warn(`⚠️  Found Cursor Agent CLI at ${candidate} but could not execute it:`, err);
      }
      return undefined;
    }
  }

  private async installCursorCli(): Promise<void> {
    const scriptUrl = this.state.installScriptUrl;
    const quotedUrl = `'${scriptUrl.replace(/'/g, "'\\''")}'`;
    const cmd = `curl ${quotedUrl} -fsS | bash`;
    try {
      await execFileAsync('bash', ['-lc', cmd], {
        env: this.buildEnv(),
        maxBuffer: 20 * 1024 * 1024,
        timeout: 5 * 60_000,
      });
      console.log('✅ Cursor Agent CLI installation finished');
    } catch (err) {
      console.warn('⚠️  Cursor Agent CLI installation failed:', (err as Error).message);
    }
  }

  private buildEnv(): NodeJS.ProcessEnv {
    const env = { ...process.env };
    const localBin = path.join(os.homedir(), '.local', 'bin');
    const currentPath = env.PATH ?? '';
    const segments = currentPath.split(path.delimiter).filter(Boolean);
    if (!segments.includes(localBin)) {
      segments.unshift(localBin);
      env.PATH = segments.join(path.delimiter);
    }
    return env;
  }

  private async tryLogin(command: string): Promise<boolean> {
    const loginArgs = ['mcp', 'login', this.state.serverKey, ...(this.state.loginArgs ?? [])];
    try {
      console.log(`→ Authenticating Cursor Agent CLI against MCP server ${this.state.serverKey}`);
      await execFileAsync(command, loginArgs, {
        env: this.buildEnv(),
        maxBuffer: 5 * 1024 * 1024,
        timeout: 60_000,
      });
      return true;
    } catch (err) {
      const formatted = formatExecError(err);
      console.warn('⚠️  Cursor Agent CLI login failed (continuing):', formatted);
      if (this.state.allowStubFallback) {
        console.warn('⚠️  Falling back to Cursor Agent CLI stub after login failure.');
        this.useStub = true;
      }
      return false;
    }
  }
}

export function createCursorCliController(
  requirement: CursorCliAgentRequirement
): CursorCliController {
  const configPath = expandHome(requirement.configPath ?? DEFAULT_CONFIG_PATH);
  const projectConfigPath = path.resolve('.cursor', 'mcp.json');
  const serverKey = requirement.serverKey ?? DEFAULT_SERVER_KEY;
  const url = requirement.url ?? DEFAULT_URL;
  const startupTimeoutSec = requirement.startupTimeoutSec ?? DEFAULT_TIMEOUT_SEC;
  const command = requirement.command ?? DEFAULT_COMMAND;
  const launchArgs = requirement.launchArgs ?? DEFAULT_LAUNCH_ARGS;
  const installIfMissing = requirement.installIfMissing ?? true;
  const installScriptUrl = requirement.installScriptUrl ?? DEFAULT_INSTALL_SCRIPT_URL;
  const autoLogin = requirement.autoLogin ?? true;
  const envUseStub = parseEnvBoolean(process.env.MCPX_CURSOR_CLI_USE_STUB);
  const envAllowStubFallback = parseEnvBoolean(process.env.MCPX_CURSOR_CLI_ALLOW_STUB);
  const envCi = parseEnvBoolean(process.env.CI);
  const useStub = requirement.useStub ?? envUseStub ?? false;
  const allowStubFallback =
    requirement.allowStubFallback ?? envAllowStubFallback ?? (useStub || (envCi ?? false));

  const state: CursorCliState = {
    configPath,
    serverKey,
    url,
    startupTimeoutSec,
    command,
    launchArgs,
    installIfMissing,
    installScriptUrl,
    autoLogin,
    loginArgs: requirement.loginArgs,
    useStub,
    allowStubFallback,
    projectConfigPath,
  };

  return new CursorCliController(requirement, state);
}

function isSupportedPlatform(): boolean {
  return process.platform === 'darwin' || process.platform === 'linux';
}

async function fileExists(file: string): Promise<boolean> {
  try {
    await fs.promises.access(file, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

function formatExecError(err: unknown): string {
  if (!err || typeof err !== 'object') {
    return String(err ?? 'unknown error');
  }

  const execErr = err as ExecFileException & { stdout?: string; stderr?: string };
  const segments = [execErr.stdout, execErr.stderr]
    .map((segment) => (typeof segment === 'string' ? segment.trim() : ''))
    .filter((segment) => segment.length > 0);

  if (segments.length > 0) {
    return segments.join('\n');
  }

  return execErr.message ?? 'unknown error';
}

function parseEnvBoolean(value: string | undefined): boolean | undefined {
  if (value === undefined) return undefined;
  const normalized = value.trim().toLowerCase();
  if (normalized.length === 0) return undefined;
  if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
  if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
  return undefined;
}
