import { existsSync } from 'fs';
import os from 'os';
import path from 'path';
import { randomBytes } from 'crypto';
import { spawnSync } from 'child_process';
import * as nodePty from 'node-pty';
import { EventEmitter } from 'events';
import { broadcastToClients } from '../websocket.js';
import {
  buildCliSessionExecuteCommand,
  type CliSessionShellKind,
  type CliSessionResumeStrategy
} from './cli-session-command-builder.js';
import { getCliSessionPolicy } from './cli-session-policy.js';
import { appendCliSessionAudit } from './cli-session-audit.js';
import { getLaunchConfig } from './cli-launch-registry.js';
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
import { loadEndpointSettings } from '../../config/cli-settings-manager.js';
import { getToolConfig } from '../../tools/claude-cli-tools.js';
import { generateExecutionId } from '../../tools/cli-executor.js';

export interface CliSession {
  sessionKey: string;
  shellKind: CliSessionShellKind;
  workingDir: string;
  tool?: string;
  model?: string;
  resumeKey?: string;
  createdAt: string;
  updatedAt: string;
  isPaused: boolean;
  /** When set, this session is a native CLI interactive process (not a shell). */
  cliTool?: string;
}

export interface CreateCliSessionOptions {
  workingDir: string;
  cols?: number;
  rows?: number;
  /** Shell to use for spawning CLI tools on Windows. */
  preferredShell?: 'bash' | 'pwsh' | 'cmd';
  tool?: string;
  model?: string;
  resumeKey?: string;
  /** Launch mode for native CLI sessions. */
  launchMode?: 'default' | 'yolo';
  /** Settings endpoint ID for injecting env vars and settings into CLI process. */
  settingsEndpointId?: string;
}

export interface ExecuteInCliSessionOptions {
  tool: string;
  prompt: string;
  mode?: 'analysis' | 'write' | 'auto';
  model?: string;
  workingDir?: string;
  category?: 'user' | 'internal' | 'insight';
  resumeKey?: string;
  resumeStrategy?: CliSessionResumeStrategy;
  /** Instruction type for native CLI sessions. */
  instructionType?: InstructionType;
  /** Skill name for instructionType='skill'. */
  skillName?: string;
}

export interface CliSessionOutputEvent {
  sessionKey: string;
  data: string;
  timestamp: string;
}

interface CliSessionInternal extends CliSession {
  pty: nodePty.IPty;
  buffer: string[];
  bufferBytes: number;
  lastActivityAt: number;
  isPaused: boolean;
}

function nowIso(): string {
  return new Date().toISOString();
}

function createSessionKey(): string {
  const suffix = randomBytes(4).toString('hex');
  return `cli-session-${Date.now()}-${suffix}`;
}

function normalizeWorkingDir(workingDir: string): string {
  return path.resolve(workingDir);
}

function findGitBashExe(): string | null {
  const candidates = [
    'C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe',
    'C:\\\\Program Files\\\\Git\\\\usr\\\\bin\\\\bash.exe',
    'C:\\\\Program Files (x86)\\\\Git\\\\bin\\\\bash.exe',
    'C:\\\\Program Files (x86)\\\\Git\\\\usr\\\\bin\\\\bash.exe'
  ];
  for (const candidate of candidates) {
    if (existsSync(candidate)) return candidate;
  }
  try {
    const where = spawnSync('where', ['bash'], { encoding: 'utf8', windowsHide: true });
    if (where.status === 0) {
      const lines = (where.stdout || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);
      const gitBash = lines.find(l => /\\Git\\.*\\bash\.exe$/i.test(l));
      return gitBash || (lines[0] || null);
    }
  } catch {
    // ignore
  }
  return null;
}

function isWslAvailable(): boolean {
  try {
    const probe = spawnSync('wsl.exe', ['-e', 'bash', '-lc', 'echo ok'], {
      encoding: 'utf8',
      windowsHide: true,
      timeout: 1500
    });
    return probe.status === 0;
  } catch {
    return false;
  }
}

function pickShell(preferred: 'bash' | 'pwsh'): { shellKind: CliSessionShellKind; file: string; args: string[] } {
  if (os.platform() === 'win32') {
    if (preferred === 'bash') {
      if (isWslAvailable()) {
        return { shellKind: 'wsl-bash', file: 'wsl.exe', args: ['-e', 'bash', '-l', '-i'] };
      }
      const gitBash = findGitBashExe();
      if (gitBash) {
        return { shellKind: 'git-bash', file: gitBash, args: ['-l', '-i'] };
      }
    }

    // Fallback: PowerShell (pwsh preferred, windows powershell as final)
    const pwsh = spawnSync('where', ['pwsh'], { encoding: 'utf8', windowsHide: true });
    if (pwsh.status === 0) {
      return { shellKind: 'pwsh', file: 'pwsh', args: ['-NoLogo'] };
    }
    return { shellKind: 'pwsh', file: 'powershell', args: ['-NoLogo'] };
  }

  // Non-Windows: keep it simple (bash-first)
  if (preferred === 'pwsh') {
    return { shellKind: 'pwsh', file: 'pwsh', args: ['-NoLogo'] };
  }
  return { shellKind: 'git-bash', file: 'bash', args: ['-l', '-i'] };
}

function toWslPath(winPath: string): string {
  const normalized = winPath.replace(/\\/g, '/');
  const driveMatch = normalized.match(/^([a-zA-Z]):\/(.*)$/);
  if (!driveMatch) return normalized;
  return `/mnt/${driveMatch[1].toLowerCase()}/${driveMatch[2]}`;
}

export class CliSessionManager {
  private sessions = new Map<string, CliSessionInternal>();
  private resumeKeyLastExecution = new Map<string, string>();
  private projectRoot: string;
  private emitter = new EventEmitter();
  private maxBufferBytes: number;
  private idleTimeoutMs: number;
  private reaperTimer: NodeJS.Timeout | null = null;

  constructor(projectRoot: string) {
    this.projectRoot = projectRoot;
    const policy = getCliSessionPolicy();
    this.maxBufferBytes = policy.maxBufferBytes;
    this.idleTimeoutMs = policy.idleTimeoutMs;

    if (this.idleTimeoutMs > 0) {
      this.reaperTimer = setInterval(() => {
        const reaped = this.closeIdleSessions(this.idleTimeoutMs);
        for (const sessionKey of reaped) {
          appendCliSessionAudit({
            type: 'session_idle_reaped',
            timestamp: nowIso(),
            projectRoot: this.projectRoot,
            sessionKey,
          });
        }
      }, 60_000);
      this.reaperTimer.unref?.();
    }
  }

  listSessions(): CliSession[] {
    return Array.from(this.sessions.values()).map(({ pty: _pty, buffer: _buffer, bufferBytes: _bytes, ...rest }) => rest);
  }

  getProjectRoot(): string {
    return this.projectRoot;
  }

  hasSession(sessionKey: string): boolean {
    return this.sessions.has(sessionKey);
  }

  getSession(sessionKey: string): CliSession | null {
    const session = this.sessions.get(sessionKey);
    if (!session) return null;
    const { pty: _pty, buffer: _buffer, bufferBytes: _bytes, ...rest } = session;
    return rest;
  }

  getBuffer(sessionKey: string): string {
    const session = this.sessions.get(sessionKey);
    if (!session) return '';
    return session.buffer.join('');
  }

  createSession(options: CreateCliSessionOptions): CliSession {
    const workingDir = normalizeWorkingDir(options.workingDir);
    const sessionKey = createSessionKey();
    const createdAt = nowIso();

    let shellKind: CliSessionShellKind;
    let file: string;
    let args: string[];
    let cliTool: string | undefined;

    // Load settings endpoint env vars and extra args if specified
    let endpointEnv: Record<string, string> = {};
    let endpointExtraArgs: string[] = [];

    if (options.settingsEndpointId) {
      try {
        const endpoint = loadEndpointSettings(options.settingsEndpointId);
        if (endpoint) {
          // Merge env vars (skip undefined/empty values)
          for (const [key, value] of Object.entries(endpoint.settings.env)) {
            if (value !== undefined && value !== '') {
              endpointEnv[key] = value;
            }
          }
          // Provider-specific argument injection
          const provider = endpoint.provider || 'claude';
          if (provider === 'claude' && 'settingsFile' in endpoint.settings && endpoint.settings.settingsFile) {
            endpointExtraArgs.push('--settings', endpoint.settings.settingsFile);
          } else if (provider === 'codex' && 'profile' in endpoint.settings && endpoint.settings.profile) {
            endpointExtraArgs.push('--profile', endpoint.settings.profile);
          }
          // Gemini: env vars only, no extra CLI flags
        }
      } catch (err) {
        console.warn('[CliSessionManager] Failed to load settings endpoint:', options.settingsEndpointId, err);
      }
    } else if (options.tool) {
      // Fallback: read settingsFile from cli-tools.json for the tool
      try {
        const toolConfig = getToolConfig(this.projectRoot, options.tool);
        if (options.tool === 'claude' && toolConfig.settingsFile) {
          endpointExtraArgs.push('--settings', toolConfig.settingsFile);
        }
      } catch (err) {
        // Non-fatal: continue without settings file
      }
    }

    if (options.tool) {
      // Native CLI interactive session: spawn the CLI process directly
      const launchMode = options.launchMode ?? 'default';
      const config = getLaunchConfig(options.tool, launchMode);
      cliTool = options.tool;

      // Append endpoint-specific extra args (e.g., --settings for claude)
      const allArgs = [...config.args, ...endpointExtraArgs];

      // Build the full command string with arguments
      const fullCommand = allArgs.length > 0
        ? `${config.command} ${allArgs.join(' ')}`
        : config.command;

      // On Windows, CLI tools installed via npm are typically .cmd files.
      // node-pty cannot spawn .cmd files directly, so we need a shell wrapper.
      // On Unix systems, direct spawn usually works.
      if (os.platform() === 'win32') {
        // Use user's preferred shell (default to cmd for reliability)
        const shell = options.preferredShell ?? 'cmd';

        if (shell === 'cmd') {
          shellKind = 'cmd';
          file = 'cmd.exe';
          args = ['/c', fullCommand];
        } else if (shell === 'pwsh') {
          shellKind = 'pwsh';
          // Check for PowerShell Core (pwsh) or fall back to Windows PowerShell
          const pwshPath = spawnSync('where', ['pwsh'], { encoding: 'utf8', windowsHide: true });
          if (pwshPath.status === 0) {
            file = 'pwsh';
          } else {
            file = 'powershell';
          }
          args = ['-NoLogo', '-Command', fullCommand];
        } else {
          // bash - try git-bash or WSL
          const gitBash = findGitBashExe();
          if (gitBash) {
            shellKind = 'git-bash';
            file = gitBash;
            args = ['-l', '-i', '-c', fullCommand];
          } else if (isWslAvailable()) {
            shellKind = 'wsl-bash';
            file = 'wsl.exe';
            args = ['-e', 'bash', '-l', '-i', '-c', fullCommand];
          } else {
            // Fall back to cmd if no bash available
            shellKind = 'cmd';
            file = 'cmd.exe';
            args = ['/c', fullCommand];
          }
        }
      } else {
        // Unix: direct spawn works for most CLI tools
        shellKind = 'git-bash';
        file = config.command;
        args = allArgs;
      }

    } else {
      // Legacy shell session: spawn bash/pwsh
      // Note: 'cmd' is for CLI tools only, for legacy shells we default to bash
      const shellPreference = options.preferredShell ?? 'bash';
      const preferredShell = shellPreference === 'cmd' ? 'bash' : shellPreference;
      const picked = pickShell(preferredShell as 'bash' | 'pwsh');
      shellKind = picked.shellKind;
      file = picked.file;
      args = picked.args;
    }

    // Merge endpoint env vars with process.env (endpoint overrides process.env)
    const spawnEnv: Record<string, string | undefined> = {
      ...(process.env as Record<string, string>),
      ...endpointEnv,
    };

    // Unset CLAUDECODE to allow nested Claude Code sessions (SDK/subagent use case)
    // See: https://github.com/anthropics/claude-agent-sdk-python/issues/573
    delete spawnEnv.CLAUDECODE;

    let pty: nodePty.IPty;
    try {
      pty = nodePty.spawn(file, args, {
        name: 'xterm-256color',
        cols: options.cols ?? 120,
        rows: options.rows ?? 30,
        cwd: workingDir,
        env: spawnEnv
      });
    } catch (spawnError: unknown) {
      const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError);
      const toolInfo = options.tool ? `tool '${options.tool}' (` : '';
      const shellInfo = options.tool ? `)` : `shell '${file}'`;
      throw new Error(`Failed to spawn ${toolInfo}${shellInfo}: ${errorMsg}. Ensure the CLI tool is installed and available in PATH.`);
    }

    const session: CliSessionInternal = {
      sessionKey,
      shellKind,
      workingDir,
      tool: options.tool,
      model: options.model,
      resumeKey: options.resumeKey,
      createdAt,
      updatedAt: createdAt,
      pty,
      buffer: [],
      bufferBytes: 0,
      lastActivityAt: Date.now(),
      isPaused: false,
      cliTool,
    };

    pty.onData((data) => {
      this.appendToBuffer(sessionKey, data);
      const now = Date.now();
      const s = this.sessions.get(sessionKey);
      if (s) {
        s.updatedAt = nowIso();
        s.lastActivityAt = now;
      }

      this.emitter.emit('output', {
        sessionKey,
        data,
        timestamp: nowIso(),
      } satisfies CliSessionOutputEvent);
      broadcastToClients({
        type: 'CLI_SESSION_OUTPUT',
        payload: {
          sessionKey,
          data,
          timestamp: nowIso(),
          projectPath: this.projectRoot,
        } satisfies CliSessionOutputEvent & { projectPath: string }
      });
    });

    pty.onExit(({ exitCode, signal }) => {
      this.sessions.delete(sessionKey);
      broadcastToClients({
        type: 'CLI_SESSION_CLOSED',
        payload: {
          sessionKey,
          exitCode,
          signal,
          timestamp: nowIso(),
          projectPath: this.projectRoot,
        }
      });
    });

    this.sessions.set(sessionKey, session);

    // WSL often ignores Windows cwd; best-effort cd to mounted path.
    // Only for legacy shell sessions, not native CLI sessions.
    if (!cliTool && shellKind === 'wsl-bash') {
      const wslCwd = toWslPath(workingDir.replace(/\\/g, '/'));
      this.sendText(sessionKey, `cd ${wslCwd}`, true);
    }

    broadcastToClients({
      type: 'CLI_SESSION_CREATED',
      payload: {
        session: this.getSession(sessionKey),
        timestamp: nowIso(),
        projectPath: this.projectRoot,
      }
    });

    return this.getSession(sessionKey)!;
  }

  sendText(sessionKey: string, text: string, appendNewline: boolean): void {
    const session = this.sessions.get(sessionKey);
    if (!session) {
      throw new Error(`Session not found: ${sessionKey}`);
    }
    session.updatedAt = nowIso();
    session.lastActivityAt = Date.now();
    session.pty.write(text);
    if (appendNewline) {
      session.pty.write('\r');
    }
  }

  resize(sessionKey: string, cols: number, rows: number): void {
    const session = this.sessions.get(sessionKey);
    if (!session) {
      throw new Error(`Session not found: ${sessionKey}`);
    }
    session.updatedAt = nowIso();
    session.lastActivityAt = Date.now();
    session.pty.resize(cols, rows);
  }

  close(sessionKey: string): void {
    const session = this.sessions.get(sessionKey);
    if (!session) return;
    session.updatedAt = nowIso();
    session.lastActivityAt = Date.now();
    try {
      session.pty.kill();
    } finally {
      this.sessions.delete(sessionKey);
      broadcastToClients({
        type: 'CLI_SESSION_CLOSED',
        payload: {
          sessionKey,
          timestamp: nowIso(),
          projectPath: this.projectRoot,
        }
      });
    }
  }

  pauseSession(sessionKey: string): void {
    const session = this.sessions.get(sessionKey);
    if (!session) {
      throw new Error(`Session not found: ${sessionKey}`);
    }
    if (session.isPaused) {
      throw new Error(`Session already paused: ${sessionKey}`);
    }
    const pid = session.pty.pid;
    if (pid === undefined) {
      throw new Error(`Session PTY has no PID: ${sessionKey}`);
    }
    try {
      process.kill(pid, 'SIGSTOP');
      session.isPaused = true;
      session.updatedAt = nowIso();
      broadcastToClients({
        type: 'CLI_SESSION_PAUSED',
        payload: {
          sessionKey,
          timestamp: nowIso(),
          projectPath: this.projectRoot,
        }
      });
    } catch (err) {
      throw new Error(`Failed to pause session ${sessionKey}: ${(err as Error).message}`);
    }
  }

  resumeSession(sessionKey: string): void {
    const session = this.sessions.get(sessionKey);
    if (!session) {
      throw new Error(`Session not found: ${sessionKey}`);
    }
    if (!session.isPaused) {
      throw new Error(`Session is not paused: ${sessionKey}`);
    }
    const pid = session.pty.pid;
    if (pid === undefined) {
      throw new Error(`Session PTY has no PID: ${sessionKey}`);
    }
    try {
      process.kill(pid, 'SIGCONT');
      session.isPaused = false;
      session.updatedAt = nowIso();
      session.lastActivityAt = Date.now();
      broadcastToClients({
        type: 'CLI_SESSION_RESUMED',
        payload: {
          sessionKey,
          timestamp: nowIso(),
          projectPath: this.projectRoot,
        }
      });
    } catch (err) {
      throw new Error(`Failed to resume session ${sessionKey}: ${(err as Error).message}`);
    }
  }

  execute(sessionKey: string, options: ExecuteInCliSessionOptions): { executionId: string; command: string } {
    const session = this.sessions.get(sessionKey);
    if (!session) {
      throw new Error(`Session not found: ${sessionKey}`);
    }
    session.updatedAt = nowIso();
    session.lastActivityAt = Date.now();

    const resumeKey = options.resumeKey ?? session.resumeKey;
    const resumeMapKey = resumeKey ? `${options.tool}:${resumeKey}` : null;
    const prevExecutionId = resumeMapKey ? this.resumeKeyLastExecution.get(resumeMapKey) : undefined;

    const executionId = resumeKey
      ? `${resumeKey}-${Date.now()}`
      : generateExecutionId(options.tool);

    let command: string;

    if (session.cliTool) {
      // Native CLI session: assemble instruction and sendText directly
      const instructionType = options.instructionType ?? 'prompt';
      command = assembleInstruction(session.cliTool, instructionType, options.prompt, options.skillName);
      this.sendText(sessionKey, command, true);
    } else {
      // Legacy shell session: build ccw cli pipe command
      const result = buildCliSessionExecuteCommand({
        projectRoot: this.projectRoot,
        shellKind: session.shellKind,
        tool: options.tool,
        prompt: options.prompt,
        mode: options.mode,
        model: options.model,
        workingDir: options.workingDir ?? session.workingDir,
        category: options.category,
        resumeStrategy: options.resumeStrategy,
        prevExecutionId,
        executionId
      });
      command = result.command;
      this.sendText(sessionKey, command, true);
    }

    // Best-effort: preemptively update mapping so subsequent queue items can chain.
    if (resumeMapKey) {
      this.resumeKeyLastExecution.set(resumeMapKey, executionId);
    }

    broadcastToClients({
      type: 'CLI_SESSION_EXECUTE',
      payload: { sessionKey, executionId, command, timestamp: nowIso() }
    });

    return { executionId, command };
  }

  private appendToBuffer(sessionKey: string, chunk: string): void {
    const session = this.sessions.get(sessionKey);
    if (!session) return;

    session.buffer.push(chunk);
    session.bufferBytes += Buffer.byteLength(chunk, 'utf8');

    while (session.bufferBytes > this.maxBufferBytes && session.buffer.length > 0) {
      const removed = session.buffer.shift();
      if (removed) session.bufferBytes -= Buffer.byteLength(removed, 'utf8');
    }
  }

  onOutput(listener: (event: CliSessionOutputEvent) => void): () => void {
    const handler = (event: CliSessionOutputEvent) => listener(event);
    this.emitter.on('output', handler);
    return () => this.emitter.off('output', handler);
  }

  closeIdleSessions(idleTimeoutMs: number): string[] {
    if (idleTimeoutMs <= 0) return [];
    const now = Date.now();
    const closed: string[] = [];
    for (const s of this.sessions.values()) {
      if (now - s.lastActivityAt >= idleTimeoutMs) {
        this.close(s.sessionKey);
        closed.push(s.sessionKey);
      }
    }
    return closed;
  }
}

const managersByRoot = new Map<string, CliSessionManager>();

export function getCliSessionManager(projectRoot: string = process.cwd()): CliSessionManager {
  const resolved = path.resolve(projectRoot);
  const existing = managersByRoot.get(resolved);
  if (existing) return existing;
  const created = new CliSessionManager(resolved);
  managersByRoot.set(resolved, created);
  return created;
}

/**
 * Find the manager that owns a given sessionKey.
 * Useful for cross-workspace routing (tmux-like send) where the executor
 * may not share the same workflowDir/projectRoot as the target session.
 */
export function findCliSessionManager(sessionKey: string): CliSessionManager | null {
  for (const manager of managersByRoot.values()) {
    if (manager.hasSession(sessionKey)) return manager;
  }
  return null;
}
