import { Type } from '@sinclair/typebox';
import * as path from 'path';
import sharp = require('sharp');
import { AgentToolWithMeta, imageResult, textResult } from '../common.js';
import { registerSchema } from '../catalog.js';
import { withFileMutationQueueOps } from '../file_mutation_queue.js';
import { resolveToolPathFromParams, resolveToolReadPathFromParams } from '../path_utils.js';
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from '../truncate.js';
import { getDefaultOperations, type ToolOperations, type FileOperations } from '../operations/index.js';

const IMAGE_EXTENSIONS = new Set([
  '.png',
  '.jpg',
  '.jpeg',
  '.gif',
  '.webp',
  '.bmp',
  '.svg',
  '.ico',
]);

const MAX_INLINE_IMAGE_BASE64_BYTES = 4.5 * 1024 * 1024;
const MAX_IMAGE_DIMENSION = 2000;
const IMAGE_JPEG_QUALITIES = [82, 70, 55, 40];

const ReadFileParams = Type.Object({
  path: Type.String({ description: '文件绝对路径' }),
  encoding: Type.Optional(Type.String({ description: '编码，默认 utf-8' })),
  offset: Type.Optional(Type.Number({ description: '起始行号，从 1 开始' })),
  limit: Type.Optional(Type.Number({ description: '最多读取多少行' })),
});

export function createReadFileTool(opts?: { operations?: ToolOperations }): AgentToolWithMeta<typeof ReadFileParams, unknown> {
  const ops = opts?.operations ?? getDefaultOperations();
  return {
    name: 'read_file',
    label: '读取文件',
    description: `读取指定路径的文件内容。文本文件默认截断到 ${DEFAULT_MAX_LINES} 行或 ${formatSize(DEFAULT_MAX_BYTES)}，可通过 offset/limit 分段继续读取。图片与二进制文件会返回说明而不是原始内容。`,
    parameters: ReadFileParams,
    async execute(_id, params) {
      try {
        const targetPath = resolveToolReadPathFromParams(params.path, params as Record<string, unknown>);
        const normalizedEncoding = normalizeEncoding(params.encoding);
        if (await ops.file.isDirectory(targetPath).catch(() => false)) {
          return textResult(`读取文件失败: ${targetPath} 不是普通文件`);
        }
        await ops.file.access(targetPath, 'read');

        const extension = path.extname(targetPath).toLowerCase();
        if (IMAGE_EXTENSIONS.has(extension)) {
          const mimeType = detectImageMimeType(extension);
          return await readImageFile(targetPath, mimeType, ops.file);
        }

        const buffer = await ops.file.readFile(targetPath);
        if (looksLikeBinary(buffer)) {
          return textResult(`检测到二进制文件: ${targetPath}\n当前 read_file 仅适合读取文本内容，请改用更合适的工具。`);
        }

        const content = buffer.toString(normalizedEncoding);
        return textResult(formatReadFileContent(targetPath, content, params.offset, params.limit));
      } catch (err: any) {
        return textResult(`读取文件失败: ${err.message}`);
      }
    },
  };
}

export const readFileTool = createReadFileTool();

const WriteFileParams = Type.Object({
  path: Type.String({ description: '文件绝对路径' }),
  content: Type.String({ description: '要写入的内容' }),
  append: Type.Optional(Type.Boolean({ description: '追加模式：true 则追加到文件末尾而非覆盖，默认 false' })),
});

export function createWriteFileTool(opts?: { operations?: ToolOperations }): AgentToolWithMeta<typeof WriteFileParams, unknown> {
  const ops = opts?.operations ?? getDefaultOperations();
  return {
    name: 'write_file',
    label: '写入文件',
    description: '将内容写入指定路径的文件。默认覆盖写入；设置 append=true 可追加到文件末尾（适合分块写入大文件）。文件不存在时自动创建父目录。',
    parameters: WriteFileParams,
    async execute(_id, params) {
      try {
        const targetPath = resolveToolPathFromParams(params.path, params as Record<string, unknown>);
        const isAppend = params.append === true;
        await withFileMutationQueueOps(targetPath, ops.file, async () => {
          await ops.file.mkdir(path.dirname(targetPath));
          if (isAppend) {
            await ops.file.appendFile(targetPath, params.content);
          } else {
            await ops.file.writeFile(targetPath, params.content);
          }
        });
        const bytes = Buffer.byteLength(params.content, 'utf-8');
        const lines = params.content === '' ? 0 : params.content.split('\n').length;
        const action = isAppend ? '追加' : '写入';
        return textResult(`write_file ${action}完成：${targetPath}\n${action}大小：${formatSize(bytes)}\n${action}行数：${lines}`);
      } catch (err: any) {
        return textResult(`写入文件失败: ${err.message}`);
      }
    },
  };
}

export const writeFileTool = createWriteFileTool();

const ListDirParams = Type.Object({
  path: Type.Optional(Type.String({ description: '目录路径，默认当前会话工作目录' })),
  limit: Type.Optional(Type.Number({ description: '最多返回多少条目录项，默认 500' })),
});

export function createListDirTool(opts?: { operations?: ToolOperations }): AgentToolWithMeta<typeof ListDirParams, unknown> {
  const ops = opts?.operations ?? getDefaultOperations();
  return {
    name: 'list_dir',
    label: '列出目录',
    description: `列出指定目录下的文件和子目录。结果按名称排序，目录会追加 / 标记，并在超过 500 条或 ${formatSize(DEFAULT_MAX_BYTES)} 时截断。`,
    parameters: ListDirParams,
    async execute(_id, params) {
      try {
        const targetPath = resolveToolPathFromParams(params.path || '.', params as Record<string, unknown>);
        const isDir = await ops.file.isDirectory(targetPath).catch(() => false);
        if (!isDir) {
          return textResult(`列出目录失败: ${targetPath} 不是目录`);
        }

        const effectiveLimit = normalizeLimit(params.limit) ?? 500;
        const entries = await ops.file.readDir(targetPath);
        const sortedEntries = [...entries].sort((left, right) =>
          left.name.localeCompare(right.name, 'zh-CN', { sensitivity: 'base' }),
        );

        const lines: string[] = [];
        let entryLimitReached = false;
        for (const entry of sortedEntries) {
          if (lines.length >= effectiveLimit) {
            entryLimitReached = true;
            break;
          }
          const suffix = entry.isDirectory ? '/' : '';
          const sizeText = entry.isDirectory ? '-' : formatSize(entry.size);
          const hiddenText = entry.name.startsWith('.') ? ' hidden' : '';
          lines.push(`${entry.name}${suffix}\t${entry.isDirectory ? 'dir' : 'file'}\t${sizeText}${hiddenText}`);
        }

        if (lines.length === 0) {
          return textResult('(空目录)');
        }

        const truncation = truncateHead(lines.join('\n'), { maxLines: Number.MAX_SAFE_INTEGER });
        let output = truncation.content;
        const notices: string[] = [];
        if (entryLimitReached) {
          notices.push(`结果数量已达到 limit=${effectiveLimit}，如需更多结果请提高 limit`);
        }
        if (truncation.truncated) {
          notices.push(`输出大小已达到 ${formatSize(DEFAULT_MAX_BYTES)}，结果已截断`);
        }
        if (notices.length > 0) {
          output += `\n\n[list_dir 结果说明]\n${notices.join('\n')}`;
        }

        return textResult(output);
      } catch (err: any) {
        return textResult(`列出目录失败: ${err.message}`);
      }
    },
  };
}

export const listDirTool = createListDirTool();

function formatReadFileContent(filePath: string, content: string, offset?: number, limit?: number): string {
  const lines = content.split('\n');
  const totalLines = lines.length;
  const startLine = normalizeOffset(offset);
  const startIndex = startLine - 1;
  if (startIndex >= totalLines) {
    throw new Error(`offset ${startLine} 超出文件总行数 ${totalLines}`);
  }

  const normalizedLimit = normalizeLimit(limit);
  const selectedContent =
    normalizedLimit === undefined
      ? lines.slice(startIndex).join('\n')
      : lines.slice(startIndex, Math.min(startIndex + normalizedLimit, totalLines)).join('\n');
  const truncation = truncateHead(selectedContent);

  if (truncation.firstLineExceedsLimit) {
    const firstLineSize = formatSize(Buffer.byteLength(lines[startIndex] ?? '', 'utf-8'));
    return [
      '[read_file 读取说明]',
      `文件：${filePath}`,
      `当前起始行：${startLine}`,
      `该行大小：${firstLineSize}`,
      `单次读取上限：${formatSize(DEFAULT_MAX_BYTES)}`,
      '建议：缩小 offset/limit 范围，或改用 bash 精确查看附近内容。',
    ].join('\n');
  }

  if (truncation.truncated) {
    const endLine = startLine + Math.max(truncation.outputLines, 1) - 1;
    const nextOffset = endLine + 1;
    const reason =
      truncation.truncatedBy === 'lines'
        ? `已达到 ${truncation.maxLines} 行限制`
        : `已达到 ${formatSize(truncation.maxBytes)} 大小限制`;
    return `${truncation.content}\n\n[read_file 分段结果]\n文件：${filePath}\n当前显示：第 ${startLine}-${endLine} 行 / 共 ${totalLines} 行\n截断原因：${reason}\n继续读取：offset=${nextOffset}`;
  }

  if (normalizedLimit !== undefined && startIndex + normalizedLimit < totalLines) {
    const endLine = Math.min(startIndex + normalizedLimit, totalLines);
    const remaining = totalLines - endLine;
    const nextOffset = endLine + 1;
    return `${truncation.content}\n\n[read_file 分段结果]\n文件：${filePath}\n当前显示：第 ${startLine}-${endLine} 行\n剩余行数：${remaining}\n继续读取：offset=${nextOffset}`;
  }

  return truncation.content;
}

function normalizeEncoding(encoding: unknown): BufferEncoding {
  if (typeof encoding !== 'string' || !encoding.trim()) {
    return 'utf-8';
  }
  return encoding.trim().toLowerCase() as BufferEncoding;
}

function normalizeOffset(offset: unknown): number {
  if (typeof offset !== 'number' || !Number.isFinite(offset)) return 1;
  return Math.max(1, Math.floor(offset));
}

function normalizeLimit(limit: unknown): number | undefined {
  if (typeof limit !== 'number' || !Number.isFinite(limit)) return undefined;
  return Math.max(1, Math.floor(limit));
}

function looksLikeBinary(buffer: Buffer): boolean {
  if (buffer.length === 0) return false;
  const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
  let suspiciousBytes = 0;
  for (const byte of sample) {
    if (byte === 0) return true;
    const isTab = byte === 9;
    const isLf = byte === 10;
    const isCr = byte === 13;
    const isPrintableAscii = byte >= 32 && byte <= 126;
    if (!isTab && !isLf && !isCr && !isPrintableAscii) {
      suspiciousBytes++;
    }
  }
  return suspiciousBytes / sample.length > 0.3;
}

function detectImageMimeType(extension: string): string {
  if (extension === '.jpg' || extension === '.jpeg') return 'image/jpeg';
  if (extension === '.png') return 'image/png';
  if (extension === '.gif') return 'image/gif';
  if (extension === '.webp') return 'image/webp';
  if (extension === '.bmp') return 'image/bmp';
  if (extension === '.svg') return 'image/svg+xml';
  if (extension === '.ico') return 'image/x-icon';
  return 'application/octet-stream';
}

function bufferToDataUrl(buffer: Buffer, mimeType: string): string {
  return `data:${mimeType};base64,${buffer.toString('base64')}`;
}

async function readImageFile(targetPath: string, mimeType: string, fileOps: FileOperations) {
  const buffer = await fileOps.readFile(targetPath);
  const originalBytes = buffer.length;
  const metadata = await readImageMetadata(buffer);

  if (canInlineImage(buffer, metadata)) {
    return imageResult(bufferToDataUrl(buffer, mimeType), mimeType, {
      text: formatImageReadText(mimeType, {
        width: metadata.width,
        height: metadata.height,
        bytes: buffer.length,
      }),
      width: metadata.width,
      height: metadata.height,
      bytes: buffer.length,
      originalBytes,
    });
  }

  const resized = await resizeImageForInline(buffer, metadata);
  if (resized) {
    return imageResult(bufferToDataUrl(resized.buffer, resized.mimeType), resized.mimeType, {
      text: formatImageReadText(resized.mimeType, {
        width: resized.width,
        height: resized.height,
        bytes: resized.buffer.length,
        originalWidth: metadata.width,
        originalHeight: metadata.height,
        originalBytes,
        resized: true,
      }),
      width: resized.width,
      height: resized.height,
      bytes: resized.buffer.length,
      originalWidth: metadata.width,
      originalHeight: metadata.height,
      originalBytes,
      resized: true,
    });
  }

  return textResult(formatImageReadText(mimeType, {
    width: metadata.width,
    height: metadata.height,
    bytes: buffer.length,
    originalBytes,
    omitted: true,
  }));
}

async function readImageMetadata(buffer: Buffer): Promise<{ width?: number; height?: number }> {
  try {
    const metadata = await sharp(buffer, { animated: false }).metadata();
    return {
      width: metadata.width,
      height: metadata.height,
    };
  } catch {
    return {};
  }
}

function canInlineImage(buffer: Buffer, metadata: { width?: number; height?: number }): boolean {
  const base64Bytes = Buffer.byteLength(buffer.toString('base64'), 'utf-8');
  const width = metadata.width ?? 0;
  const height = metadata.height ?? 0;
  return (
    base64Bytes <= MAX_INLINE_IMAGE_BASE64_BYTES &&
    (!width || width <= MAX_IMAGE_DIMENSION) &&
    (!height || height <= MAX_IMAGE_DIMENSION)
  );
}

async function resizeImageForInline(buffer: Buffer, metadata: { width?: number; height?: number }) {
  if (!metadata.width || !metadata.height) return null;

  let pipeline = sharp(buffer, { animated: false })
    .rotate()
    .resize({
      width: MAX_IMAGE_DIMENSION,
      height: MAX_IMAGE_DIMENSION,
      fit: 'inside',
      withoutEnlargement: true,
    });

  const candidates: Array<{ buffer: Buffer; mimeType: string; width?: number; height?: number }> = [];
  try {
    const png = await pipeline.clone().png().toBuffer({ resolveWithObject: true });
    candidates.push({
      buffer: png.data,
      mimeType: 'image/png',
      width: png.info.width,
      height: png.info.height,
    });
  } catch {
    // Continue with JPEG candidates below.
  }

  for (const quality of IMAGE_JPEG_QUALITIES) {
    try {
      const jpg = await pipeline.clone().jpeg({ quality, mozjpeg: true }).toBuffer({ resolveWithObject: true });
      candidates.push({
        buffer: jpg.data,
        mimeType: 'image/jpeg',
        width: jpg.info.width,
        height: jpg.info.height,
      });
    } catch {
      // Try the next quality.
    }
  }

  const sorted = candidates
    .filter(candidate => Buffer.byteLength(candidate.buffer.toString('base64'), 'utf-8') <= MAX_INLINE_IMAGE_BASE64_BYTES)
    .sort((left, right) => left.buffer.length - right.buffer.length);

  const best = sorted[0];
  if (!best || !best.width || !best.height) return null;
  return best;
}

function formatImageReadText(
  mimeType: string,
  details: {
    width?: number;
    height?: number;
    bytes?: number;
    originalWidth?: number;
    originalHeight?: number;
    originalBytes?: number;
    resized?: boolean;
    omitted?: boolean;
  },
): string {
  const lines = [`Read image file [${mimeType}]`];
  if (details.width && details.height) {
    lines.push(`Dimensions: ${details.width}x${details.height}`);
  }
  if (typeof details.bytes === 'number') {
    lines.push(`Size: ${formatSize(details.bytes)}`);
  }
  if (details.resized && details.originalWidth && details.originalHeight) {
    const scale = details.originalWidth / details.width!;
    lines.push(
      `[Image: original ${details.originalWidth}x${details.originalHeight}, displayed at ${details.width}x${details.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`
    );
  }
  if (details.omitted) {
    lines.push('[Image omitted: could not be safely inlined under the image size and dimension limits.]');
  }
  return lines.join('\n');
}

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

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

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