import fs from 'fs';
import path from 'path';
import os from 'os';
import { promisify } from 'util';
import { execFile } from 'child_process';
import axios from 'axios';
import { setTimeout as delay } from 'timers/promises';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { ClaudeDesktopAgentRequirement } from '../types';
import { AiAgentController, AgentPreparationResult } from './types';

const execFileAsync = promisify(execFile);

const DEFAULT_CONFIG_PATH = path.join(
  os.homedir(),
  'Library',
  'Application Support',
  'Claude',
  'claude_desktop_config.json'
);
const DEFAULT_SERVER_KEY = 'mcpx';
const DEFAULT_CONSUMER_TAG = 'Claude';
const DEFAULT_COMMAND = 'npx';
const DEFAULT_ARGS = [
  'mcp-remote@0.1.36',
  'http://localhost:9000/mcp',
  '--header',
  `x-lunar-consumer-tag: ${DEFAULT_CONSUMER_TAG}`,
];
const DEFAULT_TIMEOUT_SEC = 120;
const DEFAULT_MCP_URL = 'http://127.0.0.1:9000/mcp';

function expandHome(p: string): string {
  if (!p.startsWith('~')) return path.resolve(p);
  return path.resolve(path.join(os.homedir(), p.slice(1)));
}

function ensureJsonObject(value: unknown): Record<string, any> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
  return value as Record<string, any>;
}

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

async function directoryExists(dir: string): Promise<boolean> {
  try {
    const stat = await fs.promises.stat(dir);
    return stat.isDirectory();
  } catch {
    return false;
  }
}

async function quitClaude(): Promise<void> {
  try {
    await execFileAsync('osascript', ['-e', 'tell application "Claude" to quit']);
  } catch (err: any) {
    // Claude might not be running; ignore errors where osascript reports it.
    if (err?.code !== 1) {
      throw err;
    }
  }
}

async function isClaudeRunning(): Promise<boolean> {
  try {
    await execFileAsync('pgrep', ['-f', 'Claude']);
    return true;
  } catch (err: any) {
    if (err?.code === 1) return false; // pgrep returns 1 when not found
    throw err;
  }
}

async function waitForClaudeExit(timeoutMs: number): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (!(await isClaudeRunning())) return;
    await delay(500);
  }
  throw new Error('Claude Desktop did not exit within the expected timeout');
}

interface ClaudeDesktopState {
  configPath: string;
  serverKey: string;
  consumerTag: string;
  command: string;
  args: string[];
  startupTimeoutSec: number;
}

export class ClaudeDesktopController implements AiAgentController {
  public readonly requirement: ClaudeDesktopAgentRequirement;
  private readonly state: ClaudeDesktopState;
  private originalConfig?: string;
  private configExisted = false;
  private startedAgent = false;
  private useStub = false;
  private stubClient?: Client;
  private stubTransport?: SSEClientTransport;

  constructor(requirement: ClaudeDesktopAgentRequirement, state: ClaudeDesktopState) {
    this.requirement = requirement;
    this.state = state;
  }

  async prepare(): Promise<AgentPreparationResult> {
    if (process.platform !== 'darwin') {
      if (this.requirement.skipIfMissing ?? true) {
        console.warn('⚠️  Claude Desktop tests require macOS. Skipping scenario.');
        return 'skip';
      }
      throw new Error('Claude Desktop tests require macOS.');
    }

    if (!(await this.detectInstallation())) {
      if (this.requirement.skipIfMissing ?? true) {
        console.warn('⚠️  Claude Desktop not detected. Skipping scenario.');
        return 'skip';
      }
      throw new Error('Claude Desktop not detected at expected locations.');
    }

    await this.ensureConfigEntry();

    // Ensure the desktop app is not currently running before we start MCPX.
    if (await isClaudeRunning()) {
      console.log('→ Claude Desktop is running, asking it to quit before the test');
      await quitClaude();
      await waitForClaudeExit(10_000);
    }

    return 'ready';
  }

  async start(): Promise<void> {
    let launched = false;
    try {
      console.log('→ Launching Claude Desktop');
      await execFileAsync('open', ['-gj', '-a', 'Claude']);
      this.startedAgent = true;
      launched = true;
      await this.waitForAgentConnection();
      return;
    } catch (err) {
      const message = (err as Error)?.message ?? String(err);
      if (!launched) {
        console.warn(`⚠️  Failed to launch Claude Desktop (${message}). Falling back to stub.`);
      } else {
        console.warn(
          `⚠️  Claude Desktop did not connect within ${this.state.startupTimeoutSec}s (${message}). Falling back to stub.`
        );
        try {
          await quitClaude();
          await waitForClaudeExit(10_000);
        } catch (quitErr) {
          console.warn(
            '⚠️  Failed to stop Claude Desktop before stub fallback:',
            (quitErr as Error).message
          );
        }
        this.startedAgent = false;
        await delay(1_000);
      }
    }

    await this.startStub();
  }

  async cleanup(): Promise<void> {
    if (this.useStub) {
      await this.stopStub();
    }

    if (this.startedAgent) {
      try {
        console.log('→ Stopping Claude Desktop');
        await quitClaude();
        await waitForClaudeExit(10_000);
      } catch (err) {
        console.warn('⚠️  Failed to stop Claude Desktop:', (err as Error).message);
      }
    }

    await this.restoreConfig();
  }

  private async detectInstallation(): Promise<boolean> {
    const candidates = [
      '/Applications/Claude.app',
      path.join(os.homedir(), 'Applications', 'Claude.app'),
      path.dirname(this.state.configPath),
    ];
    for (const candidate of candidates) {
      if (await fileExists(candidate)) return true;
    }
    return false;
  }

  private async ensureConfigEntry(): Promise<void> {
    const { configPath, serverKey, command, args } = this.state;
    const dir = path.dirname(configPath);
    if (!(await directoryExists(dir))) {
      await fs.promises.mkdir(dir, { recursive: true });
    }

    let existing: Record<string, any> = {};
    if (await fileExists(configPath)) {
      this.configExisted = true;
      this.originalConfig = await fs.promises.readFile(configPath, 'utf8');
      try {
        existing = ensureJsonObject(JSON.parse(this.originalConfig));
      } catch (err) {
        throw new Error(
          `Failed to parse existing Claude config at ${configPath}: ${(err as Error).message}`
        );
      }
    }

    const servers = ensureJsonObject(existing.mcpServers ?? {});
    servers[serverKey] = {
      command,
      args,
    };

    const updated = {
      ...existing,
      mcpServers: servers,
    };

    const newContent = JSON.stringify(updated, null, 2) + '\n';
    if (newContent !== this.originalConfig) {
      await fs.promises.writeFile(configPath, newContent, 'utf8');
    }
  }

  private async restoreConfig(): Promise<void> {
    const { configPath } = this.state;
    try {
      if (this.configExisted) {
        if (this.originalConfig !== undefined) {
          await fs.promises.writeFile(configPath, this.originalConfig, 'utf8');
        }
      } else {
        if (await fileExists(configPath)) {
          await fs.promises.unlink(configPath);
        }
      }
    } catch (err) {
      console.warn('⚠️  Failed to restore Claude config file:', (err as Error).message);
    }
  }

  private async waitForAgentConnection(): Promise<void> {
    const { consumerTag, startupTimeoutSec } = this.state;
    const timeoutMs = startupTimeoutSec * 1000;
    const deadline = Date.now() + timeoutMs;

    console.log('→ Waiting for Claude to connect to MCPX');
    while (Date.now() < deadline) {
      try {
        const response = await axios.get('http://localhost:9000/system-state', {
          timeout: 5_000,
        });
        const clients = response.data?.connectedClients;
        if (Array.isArray(clients)) {
          const found = clients.some((client: any) => client?.consumerTag === consumerTag);
          if (found) {
            console.log('✅ Claude agent connected');
            return;
          }
        }
      } catch (err: any) {
        // Server might not be ready yet — ignore connection errors while polling
        if (err?.code !== 'ECONNREFUSED') {
          console.debug('Polling system-state failed:', err?.message ?? err);
        }
      }
      await delay(2_000);
    }

    throw new Error(
      `Timed out waiting ${startupTimeoutSec}s for Claude agent with tag "${consumerTag}" to connect`
    );
  }

  private async startStub(): Promise<void> {
    console.log('→ Launching Claude stub connection');
    this.useStub = true;

    const headers: Record<string, string> = {
      'x-lunar-consumer-tag': this.state.consumerTag,
      'user-agent': 'claude-stub/1.0.0',
    };

    const transport = new SSEClientTransport(new URL(DEFAULT_MCP_URL), {
      requestInit: { headers },
    });
    transport.onerror = (error) => {
      console.warn('⚠️  Claude stub transport error:', error.message ?? error);
    };
    this.stubTransport = transport;

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

    try {
      await client.connect(transport);
      await this.waitForAgentConnection();
    } 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 Claude stub connection');

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

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

export function createClaudeDesktopController(
  requirement: ClaudeDesktopAgentRequirement
): ClaudeDesktopController {
  const configPath = expandHome(requirement.configPath ?? DEFAULT_CONFIG_PATH);
  const serverKey = requirement.serverKey ?? DEFAULT_SERVER_KEY;
  const consumerTag = requirement.headerTag ?? DEFAULT_CONSUMER_TAG;
  const command = requirement.command ?? DEFAULT_COMMAND;
  const args = requirement.args ?? [
    DEFAULT_ARGS[0],
    DEFAULT_ARGS[1],
    DEFAULT_ARGS[2],
    `x-lunar-consumer-tag: ${consumerTag}`,
  ];

  // Ensure the args array is copied so we don't mutate shared defaults
  const startupTimeoutSec = requirement.startupTimeoutSec ?? DEFAULT_TIMEOUT_SEC;

  const state: ClaudeDesktopState = {
    configPath,
    serverKey,
    consumerTag,
    command,
    args: [...args],
    startupTimeoutSec,
  };

  if (!state.args.includes(`x-lunar-consumer-tag: ${consumerTag}`)) {
    // Ensure the consumer tag header is present unless caller explicitly provided it.
    const hasHeader = state.args.some(
      (value) => typeof value === 'string' && value.includes('x-lunar-consumer-tag')
    );
    if (!hasHeader) {
      state.args = [...state.args, '--header', `x-lunar-consumer-tag: ${consumerTag}`];
    }
  }

  return new ClaudeDesktopController(requirement, state);
}
