'use strict';
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const path = require('node:path');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');

const execFileAsync = promisify(execFile);

const IGNORED_DIRS = new Set([
  'node_modules', '.git', '.next', 'dist', 'build', '.superpowers', '__pycache__',
]);

const MAX_FILE_CONTENT = 50 * 1024; // 50KB

/**
 * Returns file tree as newline-separated relative paths.
 * Directories have trailing `/`. Excludes ignored dirs and dotfiles.
 */
async function getFileTree(cwd, maxDepth = 5) {
  const lines = [];

  async function walk(dir, depth) {
    if (depth > maxDepth) return;
    let entries;
    try {
      entries = await fsp.readdir(dir, { withFileTypes: true });
    } catch {
      return;
    }
    // Sort for deterministic output
    entries.sort((a, b) => a.name.localeCompare(b.name));
    for (const entry of entries) {
      const name = entry.name;
      // Skip dotfiles and ignored dirs
      if (name.startsWith('.')) continue;
      if (entry.isDirectory()) {
        if (IGNORED_DIRS.has(name)) continue;
        const rel = path.relative(cwd, path.join(dir, name)) + '/';
        lines.push(rel);
        await walk(path.join(dir, name), depth + 1);
      } else {
        const rel = path.relative(cwd, path.join(dir, name));
        lines.push(rel);
      }
    }
  }

  await walk(cwd, 1);
  return lines.join('\n');
}

/**
 * Reads CLAUDE.md from project dir. Returns content or empty string.
 */
async function readClaudeMd(cwd) {
  try {
    return await fsp.readFile(path.join(cwd, 'CLAUDE.md'), 'utf8');
  } catch {
    return '';
  }
}

/**
 * Runs `git log --oneline -N`. Returns string or empty.
 */
async function getGitLog(cwd, count = 20) {
  try {
    const { stdout } = await execFileAsync('git', ['log', '--oneline', `-${count}`], { cwd });
    return stdout.trim();
  } catch {
    return '';
  }
}

/**
 * Detects test command from package.json or Makefile.
 */
async function detectTestCommand(cwd) {
  // Check package.json
  try {
    const pkgRaw = await fsp.readFile(path.join(cwd, 'package.json'), 'utf8');
    const pkg = JSON.parse(pkgRaw);
    if (pkg.scripts && pkg.scripts.test) {
      return 'npm test';
    }
  } catch {
    // no package.json or invalid
  }

  // Check Makefile
  try {
    const makefile = await fsp.readFile(path.join(cwd, 'Makefile'), 'utf8');
    if (/^test:/m.test(makefile)) {
      return 'make test';
    }
  } catch {
    // no Makefile
  }

  return null;
}

/**
 * Splits request into keywords, scores files by keyword matches in filename,
 * reads top matches. Returns { filename: content } map.
 */
async function findRelevantFiles(cwd, request, maxFiles = 10, existingTree = null) {
  const keywords = request
    .toLowerCase()
    .split(/[\s,.\-_/]+/)
    .filter(w => w.length > 2);

  if (keywords.length === 0) return {};

  // Reuse existing tree if provided, otherwise fetch
  const tree = existingTree || await getFileTree(cwd);
  const files = tree.split('\n').filter(f => f && !f.endsWith('/'));

  // Score each file
  const scored = files.map(f => {
    const lower = f.toLowerCase();
    let score = 0;
    for (const kw of keywords) {
      if (lower.includes(kw)) score++;
    }
    return { file: f, score };
  });

  // Filter and sort
  const matches = scored
    .filter(s => s.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, maxFiles);

  const result = {};
  for (const { file } of matches) {
    try {
      let content = await fsp.readFile(path.join(cwd, file), 'utf8');
      if (content.length > MAX_FILE_CONTENT) {
        content = content.slice(0, MAX_FILE_CONTENT);
      }
      result[file] = content;
    } catch {
      // skip unreadable files
    }
  }
  return result;
}

/**
 * Search brain knowledge base for relevant context.
 * Uses findKnowledge (SQL LIKE) since there's no FTS on memories.
 */
function searchBrain(brain, request, limit = 10) {
  if (!brain || typeof brain.findKnowledge !== 'function') return [];
  try {
    // Extract meaningful keywords from the request
    const keywords = request
      .toLowerCase()
      .split(/[\s,.\-_/]+/)
      .filter(w => w.length > 3);
    if (keywords.length === 0) return [];

    // Search knowledge by subject for each keyword, deduplicate
    const seen = new Set();
    const results = [];
    for (const kw of keywords.slice(0, 5)) {
      const found = brain.findKnowledge({ subject: kw, status: 'active' });
      for (const item of found) {
        if (!seen.has(item.id) && results.length < limit) {
          seen.add(item.id);
          results.push(item);
        }
      }
    }
    return results;
  } catch {
    return [];
  }
}

/**
 * If mcpClient provided, list tools, find search-type tools, call up to 3.
 * Return [{ tool, result }]. Otherwise return [].
 */
async function queryMcp(mcpClient, request) {
  if (!mcpClient || typeof mcpClient.listTools !== 'function') return [];
  try {
    const tools = await mcpClient.listTools();
    const searchTools = tools.filter(t => {
      const name = (t.name || '').toLowerCase();
      const desc = (t.description || '').toLowerCase();
      return name.includes('search') || name.includes('query') || name.includes('find')
        || desc.includes('search') || desc.includes('query') || desc.includes('find');
    });

    const selected = searchTools.slice(0, 3);
    const settled = await Promise.allSettled(
      selected.map(tool =>
        callMcpSearchTool(mcpClient, tool, request)
          .then(result => ({ tool: tool.name, result }))
      )
    );
    return settled
      .filter(s => s.status === 'fulfilled')
      .map(s => s.value);
  } catch {
    return [];
  }
}

function callMcpSearchTool(mcpClient, tool, request) {
  const args = { query: request };
  if (tool.server) return mcpClient.callTool(tool.server, tool.name, args);
  return mcpClient.callTool(tool.name, args);
}

// ─── Git-Aware File Tree Snapshots (Phase 3.5) ──────────────────────────────
// Inspired by OpenCode's project snapshot system.
// Uses git ls-files (respects .gitignore), git status, git log --name-only.
// Cached per cwd with 5-minute TTL.

const _snapshotCache = new Map(); // cwd -> { snapshot, timestamp }
const SNAPSHOT_TTL_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Returns a git-aware project snapshot for the given cwd.
 * Cached per cwd. Call invalidateSnapshot(cwd) after file mutations.
 */
async function getProjectSnapshot(cwd) {
  const cached = _snapshotCache.get(cwd);
  if (cached && (Date.now() - cached.timestamp) < SNAPSHOT_TTL_MS) {
    return cached.snapshot;
  }

  let trackedFiles = [];
  let gitStatus = '';
  let recentlyChanged = [];

  // 1. Git-tracked files (respects .gitignore automatically)
  try {
    const { stdout } = await execFileAsync('git', ['ls-files'], { cwd, maxBuffer: 2 * 1024 * 1024 });
    trackedFiles = stdout.trim().split('\n').filter(Boolean);
  } catch {
    // Not a git repo or git not available — fall back to getFileTree
    const tree = await getFileTree(cwd);
    const snapshot = {
      tree,
      fileCount: tree.split('\n').filter(f => f && !f.endsWith('/')).length,
      recentlyChanged: [],
      gitStatus: '',
    };
    _snapshotCache.set(cwd, { snapshot, timestamp: Date.now() });
    return snapshot;
  }

  // 2. Working tree status (modified/added/deleted)
  try {
    const { stdout } = await execFileAsync('git', ['status', '--short'], { cwd });
    gitStatus = stdout.trim();
  } catch { /* ignore */ }

  // 3. Recently changed files (last 5 commits)
  try {
    const { stdout } = await execFileAsync('git', ['log', '--name-only', '--pretty=format:', '-5'], { cwd });
    const seen = new Set();
    recentlyChanged = stdout.trim().split('\n').filter(f => {
      if (!f || seen.has(f)) return false;
      seen.add(f);
      return true;
    });
  } catch { /* ignore */ }

  // 4. Build formatted tree string
  // Group files by directory for readable output
  const dirs = new Map(); // dir -> [filename, ...]
  for (const file of trackedFiles) {
    const dir = path.dirname(file);
    if (!dirs.has(dir)) dirs.set(dir, []);
    dirs.get(dir).push(path.basename(file));
  }

  const treeLines = [];
  const sortedDirs = [...dirs.keys()].sort();
  for (const dir of sortedDirs) {
    if (dir !== '.') treeLines.push(`${dir}/`);
    for (const file of dirs.get(dir).sort()) {
      treeLines.push(dir === '.' ? file : `${dir}/${file}`);
    }
  }

  const snapshot = {
    tree: treeLines.join('\n'),
    fileCount: trackedFiles.length,
    recentlyChanged,
    gitStatus,
  };

  _snapshotCache.set(cwd, { snapshot, timestamp: Date.now() });
  return snapshot;
}

/**
 * Invalidate the cached snapshot for a cwd.
 * Call after write_file, edit_file, or apply_patch creates/deletes files.
 */
function invalidateSnapshot(cwd) {
  _snapshotCache.delete(cwd);
}

/**
 * Calls all context-gathering functions in parallel.
 * Returns combined context object.
 */
async function assembleContext(request, cwd, { brain, mcpClient } = {}) {
  // Use git-aware snapshot instead of basic file tree
  const [snapshot, claudeMd, gitLog, testCommand, brainMemories, mcpResults] =
    await Promise.all([
      getProjectSnapshot(cwd),
      readClaudeMd(cwd),
      getGitLog(cwd),
      detectTestCommand(cwd),
      Promise.resolve(searchBrain(brain, request)),
      queryMcp(mcpClient, request),
    ]);

  const relevantFiles = await findRelevantFiles(cwd, request, 10, snapshot.tree);

  return {
    fileTree: snapshot.tree,
    claudeMd,
    gitLog,
    testCommand,
    relevantFiles,
    brainMemories,
    mcpResults,
    snapshot, // pass full snapshot for callers that need it
  };
}

// ── Instruction Auto-Loading (Phase 9f) ──
// Ported from OpenCode's instruction.ts (packages/opencode/src/session/instruction.ts).
// Walks up from a file path, finding all CLAUDE.md and AGENTS.md files.
// Returns nearest first. Stops at projectRoot.

const INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md'];

/**
 * Walk up from filePath to projectRoot, finding CLAUDE.md and AGENTS.md files.
 * Returns array of { path, content } objects, nearest first.
 *
 * @param {string} filePath - Absolute path to the file being read
 * @param {string} projectRoot - Absolute path to the project root (stop boundary)
 * @returns {Array<{ path: string, content: string }>}
 */
function discoverInstructions(filePath, projectRoot) {
  const { InstructionService } = require('./coding/instruction-service');
  return new InstructionService({ instructionFiles: INSTRUCTION_FILES })
    .discoverForFile(filePath, projectRoot, { dedupe: false })
    .instructions;
}

module.exports = {
  getFileTree,
  readClaudeMd,
  getGitLog,
  detectTestCommand,
  findRelevantFiles,
  searchBrain,
  queryMcp,
  assembleContext,
  getProjectSnapshot,
  invalidateSnapshot,
  discoverInstructions,
};
