import { Type } from '@sinclair/typebox';
import { randomBytes } from 'crypto';
import { createWriteStream, type WriteStream } from 'fs';
import iconv = require('iconv-lite');
import { tmpdir } from 'os';
import * as path from 'path';
import { AgentToolWithMeta, textResult } from '../common.js';
import { resolveToolPathFromParams } from '../path_utils.js';
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from '../truncate.js';
import { registerSchema } from '../catalog.js';
import { SkillLoaderService } from '../../service/skill_loader.js';
import { SkillSecretService } from '../../service/skill_secret.js';
import { getDefaultOperations, type ToolOperations } from '../operations/index.js';

const BashParams = Type.Object({
  command: Type.String({ description: '要执行的 shell 命令' }),
  cwd: Type.Optional(Type.String({ description: '工作目录' })),
  timeout: Type.Optional(Type.Number({ description: '超时时间（毫秒），默认 30000' })),
});

export interface BashEnvProvider {
  getAdditionalEnv(cwd: string): Promise<Record<string, string>>;
}

export interface BashToolOptions {
  operations?: ToolOperations;
  envProvider?: BashEnvProvider;
}

export function createBashTool(options: BashToolOptions = {}): AgentToolWithMeta<typeof BashParams, unknown> {
  const ops = options.operations ?? getDefaultOperations();
  const envProvider = options.envProvider;

  return {
    name: 'bash',
    label: '执行命令',
    description: `在 shell 中执行命令并返回输出。长输出会保留最后 ${DEFAULT_MAX_LINES} 行或 ${formatSize(DEFAULT_MAX_BYTES)}，完整输出会持续写入临时日志文件。`,
    parameters: BashParams,
    async execute(_id, params) {
      const timeout = params.timeout ?? 30000;
      const cwd = params.cwd
        ? resolveToolPathFromParams(params.cwd, params as Record<string, unknown>)
        : process.cwd();

      try {
        await ops.file.access(cwd, 'read');
        const env = await resolveEnv(cwd, envProvider);
        const result = await runShellCommand({
          command: params.command,
          cwd,
          timeout,
          env,
          ops,
        });
        return textResult(formatBashOutput(result.output, result.fullOutputPath, result.exitCode));
      } catch (err: any) {
        const output = err?.output ? String(err.output) : '';
        const fullOutputPath = err?.fullOutputPath ? String(err.fullOutputPath) : undefined;
        const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : null;
        const message = err?.message || String(err);

        if (output) {
          return textResult(`命令执行失败: ${message}\n${formatBashOutput(output, fullOutputPath, exitCode)}`);
        }
        return textResult(`命令执行失败: ${message}`);
      }
    },
  };
}

export const bashTool = createBashTool();

async function resolveEnv(cwd: string, envProvider?: BashEnvProvider): Promise<NodeJS.ProcessEnv | undefined> {
  if (!envProvider) return undefined;
  try {
    const additional = await envProvider.getAdditionalEnv(cwd);
    return additional;
  } catch {
    return undefined;
  }
}

async function runShellCommand(params: {
  command: string;
  cwd: string;
  timeout: number;
  env?: NodeJS.ProcessEnv;
  ops: ToolOperations;
}): Promise<{ output: string; fullOutputPath?: string; exitCode: number | null }> {
  let fullOutputPath: string | undefined;
  let fileStream: WriteStream | undefined;
  let totalBytes = 0;
  const mergedChunks: Buffer[] = [];
  const stdoutChunks: Buffer[] = [];
  const stderrChunks: Buffer[] = [];
  let chunkBytes = 0;
  const maxChunkBytes = DEFAULT_MAX_BYTES * 2;

  const ensureLogFile = () => {
    if (fileStream) return;
    fullOutputPath = path.join(tmpdir(), `neta-bash-${randomBytes(8).toString('hex')}.log`);
    fileStream = createWriteStream(fullOutputPath);
    for (const chunk of mergedChunks) {
      fileStream.write(chunk);
    }
  };

  const pushChunk = (buffer: Buffer, stream: 'stdout' | 'stderr') => {
    totalBytes += buffer.length;
    if (totalBytes > DEFAULT_MAX_BYTES) {
      ensureLogFile();
    }
    fileStream?.write(buffer);
    mergedChunks.push(buffer);
    if (stream === 'stdout') stdoutChunks.push(buffer);
    else stderrChunks.push(buffer);
    chunkBytes += buffer.length;
    while (chunkBytes > maxChunkBytes && mergedChunks.length > 1) {
      const removed = mergedChunks.shift();
      if (removed) chunkBytes -= removed.length;
    }
  };

  const buildOutput = () => {
    const stdoutText = decodeShellOutput(Buffer.concat(stdoutChunks)).trimEnd();
    const stderrText = decodeShellOutput(Buffer.concat(stderrChunks)).trimEnd();
    const parts: string[] = [];
    if (stdoutText) parts.push(stdoutText);
    if (stderrText) parts.push(`[stderr]: ${stderrText}`);
    return parts.join('\n');
  };

  const finalizeOutput = (exitCode: number | null) => {
    const output = buildOutput();
    const truncation = truncateTail(output || '(无输出)');
    if (truncation.truncated) {
      ensureLogFile();
    }
    fileStream?.end();
    return {
      output,
      fullOutputPath,
      exitCode,
    };
  };

  try {
    const result = await params.ops.process.exec(params.command, params.cwd, {
      timeout: params.timeout,
      env: params.env,
      onData: (chunk, stream) => pushChunk(Buffer.from(chunk), stream),
    });
    return finalizeOutput(result.exitCode);
  } catch (error: any) {
    const finalized = finalizeOutput(typeof error?.exitCode === 'number' ? error.exitCode : null);
    throw {
      message: error?.message || String(error),
      output: finalized.output,
      fullOutputPath: finalized.fullOutputPath,
      exitCode: finalized.exitCode,
    };
  }
}

function formatBashOutput(output: string, fullOutputPath?: string, exitCode?: number | null): string {
  const normalized = output || '(无输出)';
  const truncation = truncateTail(normalized);
  let rendered = truncation.content || '(无输出)';

  if (truncation.truncated) {
    const startLine = Math.max(1, truncation.totalLines - truncation.outputLines + 1);
    const endLine = truncation.totalLines;
    if (truncation.lastLinePartial) {
      rendered += `\n\n[显示最后 ${formatSize(truncation.outputBytes)}，完整输出: ${fullOutputPath}]`;
    } else if (truncation.truncatedBy === 'bytes') {
      rendered += `\n\n[显示第 ${startLine}-${endLine} 行，共 ${truncation.totalLines} 行，已达到 ${formatSize(truncation.maxBytes)} 限制。完整输出: ${fullOutputPath}]`;
    } else {
      rendered += `\n\n[显示第 ${startLine}-${endLine} 行，共 ${truncation.totalLines} 行。完整输出: ${fullOutputPath}]`;
    }
  } else if (fullOutputPath) {
    rendered += `\n\n[完整输出文件: ${fullOutputPath}]`;
  }

  if (typeof exitCode === 'number' && exitCode !== 0) {
    rendered += `\n\n[退出码: ${exitCode}]`;
  }

  return rendered;
}

function decodeShellOutput(buffer: Buffer): string {
  if (buffer.length === 0) return '';
  if (process.platform !== 'win32') {
    return buffer.toString('utf-8');
  }

  const utf8Text = buffer.toString('utf-8');
  const replacementCount = (utf8Text.match(/�/g) || []).length;
  if (replacementCount === 0 && !looksLikeWindowsCodePageMojibake(utf8Text)) {
    return utf8Text;
  }

  // cmd.exe localizes stderr/stdout with the active Windows code page. On zh-CN
  // machines this is commonly GBK/GB18030, so UTF-8 decoding produces mojibake.
  return iconv.decode(buffer, 'gb18030');
}

function looksLikeWindowsCodePageMojibake(value: string): boolean {
  return /[ڲǿеļϵͳҲⲿ]/.test(value);
}

export function createSkillBashEnvProvider(
  skillLoader: SkillLoaderService,
  skillSecret: SkillSecretService,
): BashEnvProvider {
  return {
    async getAdditionalEnv(cwd: string): Promise<Record<string, string>> {
      const skillName = skillLoader.resolveSkillByPath(cwd);
      if (!skillName) return {};
      return skillSecret.resolveEnv(skillName);
    },
  };
}

registerSchema({
  name: 'bash',
  toolset: 'base',
  description: bashTool.description,
  capability: 'text',
  visibility: 'tool',
  isCore: true,
  canDisable: false,
  supportsPromptHint: true,
});
