import * as fs from 'fs/promises';
import { constants } from 'fs';
import type {
  ToolOperations,
  FileOperations,
  ProcessOperations,
  SearchOperations,
  ProcessExecOptions,
  ProcessExecResult,
  SearchExecResult,
  DirEntry,
} from './types.js';
import { spawn, resolveShellConfig, type ChildProcess } from '../../../../comm/child_process.js';
import { killProcessTree, waitForChildProcess } from '../process_utils.js';

// eslint-disable-next-line no-control-regex
const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)/g;

function stripAnsi(buf: Buffer): Buffer {
  const text = buf.toString('utf-8');
  if (!text.includes('\x1b')) return buf;
  return Buffer.from(text.replace(ANSI_REGEX, ''), 'utf-8');
}

// ─── FileOperations ───

const localFileOps: FileOperations = {
  readFile: (p) => fs.readFile(p),
  writeFile: (p, c) => fs.writeFile(p, c, typeof c === 'string' ? 'utf-8' : undefined),
  appendFile: (p, c) => fs.appendFile(p, c, typeof c === 'string' ? 'utf-8' : undefined),

  async access(p, mode) {
    let flag = constants.F_OK;
    if (mode === 'read' || mode === 'readwrite') flag |= constants.R_OK;
    if (mode === 'write' || mode === 'readwrite') flag |= constants.W_OK;
    await fs.access(p, flag);
  },

  async isDirectory(p) {
    const stat = await fs.stat(p);
    return stat.isDirectory();
  },

  async readDir(p): Promise<DirEntry[]> {
    const entries = await fs.readdir(p, { withFileTypes: true });
    const results: DirEntry[] = [];
    for (const entry of entries) {
      let size = 0;
      if (!entry.isDirectory()) {
        try {
          const stat = await fs.stat(`${p}/${entry.name}`);
          size = stat.size;
        } catch { /* 忽略 stat 失败 */ }
      }
      results.push({ name: entry.name, isDirectory: entry.isDirectory(), size });
    }
    return results;
  },

  mkdir: (p) => fs.mkdir(p, { recursive: true }).then(() => {}),

  async realpath(p) {
    return fs.realpath(p);
  },
};

// ─── ProcessOperations ───

async function spawnChecked(command: string, args: string[], options: Parameters<typeof spawn>[2]): Promise<ChildProcess> {
  return new Promise<ChildProcess>((resolve, reject) => {
    let settled = false;
    const child = spawn(command, args, options);
    const onError = (error: Error) => {
      if (settled) return;
      settled = true;
      reject(error);
    };
    const onSpawn = () => {
      if (settled) return;
      settled = true;
      child.off('error', onError);
      resolve(child);
    };
    child.once('error', onError);
    child.once('spawn', onSpawn);
  });
}

async function spawnWithFallback(command: string, args: string[], options: Parameters<typeof spawn>[2]): Promise<ChildProcess> {
  try {
    return await spawnChecked(command, args, { ...options, detached: process.platform !== 'win32' });
  } catch (error: any) {
    if (error?.code !== 'EPERM') throw error;
    return await spawnChecked(command, args, { ...options, detached: false });
  }
}

const localProcessOps: ProcessOperations = {
  async exec(command: string, cwd: string, options: ProcessExecOptions): Promise<ProcessExecResult> {
    const shellConfig = resolveShellConfig();
    const shell = shellConfig.shell;
    const shellArgs = [...shellConfig.args, command];

    const env: NodeJS.ProcessEnv = { ...process.env, ...(options.env ?? {}) };

    return new Promise(async (resolve, reject) => {
      const child = await spawnWithFallback(shell, shellArgs, {
        cwd,
        env,
        stdio: ['ignore', 'pipe', 'pipe'],
      }).catch(reject);
      if (!child) return;

      let timedOut = false;
      const timeoutHandle = options.timeout && options.timeout > 0
        ? setTimeout(() => {
          timedOut = true;
          if (child.pid) killProcessTree(child.pid);
        }, options.timeout)
        : null;

      if (options.signal) {
        const onAbort = () => {
          if (child.pid) killProcessTree(child.pid);
        };
        options.signal.addEventListener('abort', onAbort, { once: true });
        child.once('close', () => options.signal!.removeEventListener('abort', onAbort));
      }

      child.stdout?.on('data', (chunk: Buffer) => options.onData(stripAnsi(Buffer.from(chunk)), 'stdout'));
      child.stderr?.on('data', (chunk: Buffer) => options.onData(stripAnsi(Buffer.from(chunk)), 'stderr'));

      try {
        const exitCode = await waitForChildProcess(child);
        if (timeoutHandle) clearTimeout(timeoutHandle);
        if (timedOut) {
          const err = new Error(`命令超时：${options.timeout}ms`);
          (err as any).code = 'ETIMEDOUT';
          reject(err);
          return;
        }
        resolve({ exitCode, signal: null });
      } catch (error) {
        if (timeoutHandle) clearTimeout(timeoutHandle);
        reject(error);
      }
    });
  },
};

// ─── SearchOperations ───

function spawnAndCollect(command: string, args: string[], cwd: string, signal?: AbortSignal): Promise<SearchExecResult> {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], cwd });
    let stdout = '';
    let stderr = '';

    if (signal) {
      const onAbort = () => { if (child.pid) killProcessTree(child.pid); };
      signal.addEventListener('abort', onAbort, { once: true });
      child.once('close', () => signal.removeEventListener('abort', onAbort));
    }

    child.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
    child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });

    child.on('error', reject);
    child.on('close', (code) => resolve({ stdout, stderr, exitCode: code }));
  });
}

const localSearchOps: SearchOperations = {
  ripgrep: (args, cwd, signal) => spawnAndCollect('rg', args, cwd, signal),
  fd: (args, cwd, signal) => spawnAndCollect('fd', args, cwd, signal),
};

// ─── 聚合导出 ───

export function createLocalToolOperations(): ToolOperations {
  return {
    file: localFileOps,
    process: localProcessOps,
    search: localSearchOps,
  };
}
