import { z } from 'zod';
import { readFile, writeFile, mkdir, readdir, unlink } from 'fs/promises';
import { dirname, resolve, relative, join } from 'path';
import { defineTool, escapeRegex, type ToolResult } from './types.js';

/**
 * Read file contents with optional line range
 */
export const readFileTool = defineTool({
  name: 'read_file',
  description:
    'Read contents of a file. Returns file content with line numbers. Use startLine/endLine to read a specific range.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    startLine: z.number().optional().describe('Start line (1-indexed, inclusive)'),
    endLine: z.number().optional().describe('End line (1-indexed, inclusive)'),
  }),

  async execute(input, context): Promise<ToolResult<{ content: string; totalLines: number }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    // Validate path is within repo
    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      const rawContent = await readFile(fullPath, 'utf-8');
      const allLines = rawContent.split('\n');
      const totalLines = allLines.length;

      // Apply line range if specified
      const start = (input.startLine ?? 1) - 1;
      const end = input.endLine ?? allLines.length;
      const selectedLines = allLines.slice(start, end);

      // Add line numbers
      const content = selectedLines
        .map((line, i) => `${start + i + 1}: ${line}`)
        .join('\n');

      return {
        success: true,
        data: { content, totalLines },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === 'ENOENT') {
        return { success: false, error: `File not found: ${input.path}` };
      }
      if (error.code === 'EISDIR') {
        return { success: false, error: `Path is a directory: ${input.path}` };
      }
      return { success: false, error: `Failed to read file: ${error.message}` };
    }
  },
});

/**
 * Write content to a file
 */
export const writeFileTool = defineTool({
  name: 'write_file',
  description: 'Write content to a file. Creates parent directories if needed. Overwrites existing files.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    content: z.string().describe('Content to write'),
  }),

  async execute(input, context): Promise<ToolResult<{ path: string; bytesWritten: number }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      // Create parent directories
      await mkdir(dirname(fullPath), { recursive: true });

      // Write file
      await writeFile(fullPath, input.content, 'utf-8');

      return {
        success: true,
        data: {
          path: input.path,
          bytesWritten: Buffer.byteLength(input.content, 'utf-8'),
        },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      return { success: false, error: `Failed to write file: ${error.message}` };
    }
  },
});

/**
 * Edit a file by replacing a specific string
 */
export const editFileTool = defineTool({
  name: 'edit_file',
  description:
    'Replace a specific string in a file with new content. The old_string must be unique in the file unless replace_all is true.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    old_string: z.string().describe('Exact string to find and replace'),
    new_string: z.string().describe('Replacement string'),
    replace_all: z.boolean().optional().describe('Replace all occurrences (default: false)'),
  }),

  async execute(
    input,
    context
  ): Promise<ToolResult<{ path: string; replacements: number }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      let content = await readFile(fullPath, 'utf-8');

      if (!content.includes(input.old_string)) {
        return {
          success: false,
          error: 'Old string not found in file. Ensure exact match including whitespace.',
          suggestions: ['Use read_file to check the exact content', 'Verify indentation matches'],
        };
      }

      // Count occurrences
      const regex = new RegExp(escapeRegex(input.old_string), 'g');
      const matches = content.match(regex) ?? [];
      const count = matches.length;

      // Check uniqueness if not replacing all
      if (!input.replace_all && count > 1) {
        return {
          success: false,
          error: `Old string appears ${count} times. Use replace_all=true or provide more context for unique match.`,
        };
      }

      // Perform replacement
      if (input.replace_all) {
        content = content.replaceAll(input.old_string, input.new_string);
      } else {
        content = content.replace(input.old_string, input.new_string);
      }

      await writeFile(fullPath, content, 'utf-8');

      return {
        success: true,
        data: {
          path: input.path,
          replacements: input.replace_all ? count : 1,
        },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === 'ENOENT') {
        return { success: false, error: `File not found: ${input.path}` };
      }
      return { success: false, error: `Failed to edit file: ${error.message}` };
    }
  },
});

/**
 * List files in a directory
 */
export const listFilesTool = defineTool({
  name: 'list_files',
  description: 'List files in a directory. Returns file paths relative to the repository root.',
  category: 'coding',
  schema: z.object({
    path: z.string().optional().describe('Directory path (defaults to repo root)'),
    recursive: z.boolean().optional().describe('List recursively (default: false)'),
    max_depth: z.number().optional().describe('Maximum recursion depth'),
  }),

  async execute(
    input,
    context
  ): Promise<ToolResult<{ files: string[]; directories: string[] }>> {
    const basePath = input.path
      ? resolve(context.repoRoot, input.path)
      : context.repoRoot;

    if (!basePath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    const files: string[] = [];
    const directories: string[] = [];

    async function listDir(dir: string, depth: number): Promise<void> {
      if (input.max_depth !== undefined && depth > input.max_depth) return;

      try {
        const entries = await readdir(dir, { withFileTypes: true });

        for (const entry of entries) {
          // Skip hidden files and common ignore patterns
          if (entry.name.startsWith('.') || entry.name === 'node_modules') {
            continue;
          }

          const fullPath = join(dir, entry.name);
          const relativePath = relative(context.repoRoot, fullPath);

          if (entry.isDirectory()) {
            directories.push(relativePath);
            if (input.recursive) {
              await listDir(fullPath, depth + 1);
            }
          } else if (entry.isFile()) {
            files.push(relativePath);
          }
        }
      } catch {
        // Ignore permission errors
      }
    }

    try {
      await listDir(basePath, 0);

      // Sort for consistent output
      files.sort();
      directories.sort();

      // Limit results
      const maxResults = 500;
      if (files.length > maxResults) {
        return {
          success: true,
          data: {
            files: files.slice(0, maxResults),
            directories: directories.slice(0, maxResults),
          },
          metadata: { truncated: true, totalFiles: files.length },
        };
      }

      return {
        success: true,
        data: { files, directories },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === 'ENOENT') {
        return { success: false, error: `Directory not found: ${input.path}` };
      }
      return { success: false, error: `Failed to list directory: ${error.message}` };
    }
  },
});

/**
 * Delete a file
 */
export const deleteFileTool = defineTool({
  name: 'delete_file',
  description: 'Delete a file from the repository. Use with caution - this cannot be undone.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file to delete'),
  }),

  async execute(input, context): Promise<ToolResult<{ path: string; deleted: boolean }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      await unlink(fullPath);

      return {
        success: true,
        data: {
          path: input.path,
          deleted: true,
        },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === 'ENOENT') {
        return { success: false, error: `File not found: ${input.path}` };
      }
      if (error.code === 'EISDIR') {
        return { success: false, error: `Path is a directory, not a file: ${input.path}` };
      }
      return { success: false, error: `Failed to delete file: ${error.message}` };
    }
  },
});

/**
 * Apply a search/replace diff to a file with fuzzy matching
 */
export const applyDiffTool = defineTool({
  name: 'apply_diff',
  description: `Apply changes to a file using search/replace blocks with fuzzy matching.

Format:
<<<<<<< SEARCH
[exact content to find - include enough context for uniqueness]
=======
[replacement content]
>>>>>>> REPLACE

You can include multiple SEARCH/REPLACE blocks in one diff.
The SEARCH content should match the file exactly (or very closely for fuzzy matching).
Include enough surrounding lines to make the match unique.

Example:
<<<<<<< SEARCH
def hello():
    print("Hello")
=======
def hello():
    print("Hello, World!")
>>>>>>> REPLACE`,
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file to modify'),
    diff: z.string().describe('Search/replace diff with <<<<<<< SEARCH ... ======= ... >>>>>>> REPLACE blocks'),
  }),

  async execute(
    input,
    context
  ): Promise<
    ToolResult<{
      path: string;
      blocksApplied: number;
      blocksTotal: number;
      warnings?: string[];
    }>
  > {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      let content = await readFile(fullPath, 'utf-8');

      // Parse the diff into search/replace blocks
      const blocks = parseDiffBlocks(input.diff);
      if (blocks.length === 0) {
        return {
          success: false,
          error:
            'No valid SEARCH/REPLACE blocks found in diff.\n' +
            'Expected format:\n' +
            '<<<<<<< SEARCH\n' +
            '[content to find]\n' +
            '=======\n' +
            '[replacement]\n' +
            '>>>>>>> REPLACE',
        };
      }

      // Apply each block
      let applied = 0;
      const warnings: string[] = [];

      for (let i = 0; i < blocks.length; i++) {
        const block = blocks[i];
        if (!block) continue;

        const { search, replace } = block;

        // Try exact match first
        if (content.includes(search)) {
          // Check uniqueness
          const count = countOccurrences(content, search);
          if (count > 1) {
            warnings.push(`Block ${i + 1}: SEARCH text found ${count} times, add more context`);
            continue;
          }
          content = content.replace(search, replace);
          applied++;
        } else {
          // Try fuzzy matching
          const matchResult = fuzzyFind(content, search);
          if (matchResult) {
            const { start, end, confidence } = matchResult;
            const CONFIDENCE_THRESHOLD = 0.85;

            if (confidence >= CONFIDENCE_THRESHOLD) {
              content = content.slice(0, start) + replace + content.slice(end);
              applied++;
            } else {
              warnings.push(
                `Block ${i + 1}: Best match confidence ${confidence.toFixed(2)} below threshold ${CONFIDENCE_THRESHOLD}`
              );
            }
          } else {
            // Provide helpful error with similar lines
            const similar = findSimilarLines(content, search);
            let errorMsg = `Block ${i + 1}: SEARCH text not found`;
            if (similar) {
              errorMsg += `\nSimilar lines:\n${similar}`;
            }
            warnings.push(errorMsg);
          }
        }
      }

      if (applied === 0) {
        return {
          success: false,
          error: `Failed to apply any blocks:\n${warnings.join('\n')}`,
        };
      }

      // Write back
      await writeFile(fullPath, content, 'utf-8');

      return {
        success: true,
        data: {
          path: input.path,
          blocksApplied: applied,
          blocksTotal: blocks.length,
          warnings: warnings.length > 0 ? warnings : undefined,
        },
      };
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === 'ENOENT') {
        return { success: false, error: `File not found: ${input.path}` };
      }
      return { success: false, error: `Failed to apply diff: ${error.message}` };
    }
  },
});

// ============================================================
// Helper Functions for ApplyDiffTool
// ============================================================

/**
 * Parse diff into search/replace blocks
 */
function parseDiffBlocks(diff: string): Array<{ search: string; replace: string }> {
  const blocks: Array<{ search: string; replace: string }> = [];
  const pattern = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;

  let match;
  while ((match = pattern.exec(diff)) !== null) {
    const search = match[1];
    const replace = match[2];
    if (search !== undefined && replace !== undefined) {
      blocks.push({ search, replace });
    }
  }

  return blocks;
}

/**
 * Count occurrences of a substring
 */
function countOccurrences(content: string, search: string): number {
  let count = 0;
  let pos = 0;
  while ((pos = content.indexOf(search, pos)) !== -1) {
    count++;
    pos += search.length;
  }
  return count;
}

/**
 * Find best fuzzy match for search text in content
 */
function fuzzyFind(
  content: string,
  searchText: string
): { start: number; end: number; confidence: number } | null {
  const searchLines = searchText.split('\n');
  const contentLines = content.split('\n');
  const searchLen = searchLines.length;

  if (searchLen === 0) return null;

  let bestMatch: { start: number; end: number; confidence: number } | null = null;
  let bestConfidence = 0;

  // Sliding window approach
  for (let i = 0; i <= contentLines.length - searchLen; i++) {
    const window = contentLines.slice(i, i + searchLen).join('\n');
    const confidence = calculateSimilarity(searchText, window);

    if (confidence > bestConfidence) {
      bestConfidence = confidence;
      // Calculate character positions
      const start = contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
      const end = start + window.length;
      bestMatch = { start, end, confidence };
    }
  }

  return bestConfidence >= 0.5 ? bestMatch : null;
}

/**
 * Calculate similarity ratio between two strings (0-1)
 * Simple implementation using longest common subsequence ratio
 */
function calculateSimilarity(a: string, b: string): number {
  if (a === b) return 1;
  if (a.length === 0 || b.length === 0) return 0;

  // Simple character-based similarity
  const aChars = new Set(a);
  const bChars = new Set(b);
  let common = 0;
  for (const char of aChars) {
    if (bChars.has(char)) common++;
  }

  const charSimilarity = (2 * common) / (aChars.size + bChars.size);

  // Line-based similarity
  const aLines = a.split('\n');
  const bLines = b.split('\n');
  let matchingLines = 0;
  const minLines = Math.min(aLines.length, bLines.length);

  for (let i = 0; i < minLines; i++) {
    const aLine = aLines[i]?.trim() ?? '';
    const bLine = bLines[i]?.trim() ?? '';
    if (aLine === bLine) matchingLines++;
  }

  const lineSimilarity = minLines > 0 ? matchingLines / Math.max(aLines.length, bLines.length) : 0;

  // Combine both metrics
  return charSimilarity * 0.3 + lineSimilarity * 0.7;
}

/**
 * Find lines in content similar to the first line of search text
 */
function findSimilarLines(content: string, searchText: string): string {
  const lines = content.split('\n');
  const searchFirst = searchText.split('\n')[0]?.trim() ?? '';

  if (!searchFirst) return '';

  const similar: string[] = [];
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i] ?? '';
    if (searchFirst.slice(0, 20) && line.includes(searchFirst.slice(0, 20))) {
      similar.push(`  Line ${i + 1}: ${line.trim().slice(0, 60)}`);
      if (similar.length >= 3) break;
    }
  }

  return similar.join('\n');
}
