'use strict';

const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');

const DEFAULT_INSTRUCTION_FILES = ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md'];
const DEFAULT_TIMEOUT_MS = 1500;
const _loadedByScope = new Map();

class InstructionService {
  constructor({
    instructionFiles = DEFAULT_INSTRUCTION_FILES,
    configured = [],
    fetchImpl = globalThis.fetch,
    timeoutMs = DEFAULT_TIMEOUT_MS,
  } = {}) {
    this.instructionFiles = instructionFiles;
    this.configured = configured;
    this.fetchImpl = fetchImpl;
    this.timeoutMs = timeoutMs;
  }

  getSystemInstructions({ projectRoot = process.cwd(), includeGlobal = true } = {}) {
    const instructions = [];
    const project = this._firstExisting(projectRoot, this.instructionFiles);
    if (project) instructions.push(project);

    if (includeGlobal) {
      for (const candidate of [
        path.join(os.homedir(), '.walle', 'instructions.md'),
        path.join(os.homedir(), '.claude', 'CLAUDE.md'),
      ]) {
        const loaded = readInstruction(candidate);
        if (loaded) instructions.push(loaded);
      }
    }
    return dedupeInstructions(instructions);
  }

  discoverForRead(filePath, {
    projectRoot = process.cwd(),
    sessionId = '',
    messageId = '',
    dedupe = true,
  } = {}) {
    const scope = `${sessionId || 'global'}:${messageId || 'session'}`;
    return this.discoverForFile(filePath, projectRoot, { scope, dedupe, skipPath: filePath });
  }

  discoverForFile(filePath, projectRoot = process.cwd(), { scope = '', dedupe = false, skipPath = '' } = {}) {
    const resolvedRoot = realpathBestEffort(projectRoot);
    const resolvedFile = realpathBestEffort(filePath);
    let dir = fs.existsSync(resolvedFile) && fs.statSync(resolvedFile).isDirectory()
      ? resolvedFile
      : path.dirname(resolvedFile);
    const found = [];
    const loaded = scope ? getLoadedSet(scope) : null;

    while (isWithin(resolvedRoot, dir)) {
      for (const name of this.instructionFiles) {
        const candidate = path.join(dir, name);
        if (path.resolve(candidate) === path.resolve(skipPath || '')) continue;
        const instruction = readInstruction(candidate);
        if (!instruction) continue;
        if (dedupe && loaded?.has(instruction.path)) continue;
        found.push(instruction);
        if (dedupe) loaded?.add(instruction.path);
      }
      const parent = path.dirname(dir);
      if (parent === dir) break;
      dir = parent;
    }
    return {
      instructions: dedupeInstructions(found),
      loadedPaths: found.map((item) => item.path),
    };
  }

  async loadConfiguredInstructions() {
    const out = [];
    for (const entry of this.configured || []) {
      if (!entry) continue;
      if (/^https?:\/\//i.test(entry)) {
        const loaded = await this._fetchInstruction(entry);
        if (loaded) out.push(loaded);
      } else if (entry.includes('*')) {
        for (const filePath of expandGlob(entry)) {
          const loaded = readInstruction(filePath);
          if (loaded) out.push(loaded);
        }
      } else {
        const loaded = readInstruction(entry);
        if (loaded) out.push(loaded);
      }
    }
    return dedupeInstructions(out);
  }

  async _fetchInstruction(url) {
    if (typeof this.fetchImpl !== 'function') return null;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeoutMs);
    try {
      const response = await this.fetchImpl(url, { signal: controller.signal });
      if (!response?.ok) return null;
      const content = await response.text();
      return { path: url, content };
    } catch {
      return null;
    } finally {
      clearTimeout(timer);
    }
  }

  _firstExisting(root, names) {
    for (const name of names) {
      const loaded = readInstruction(path.join(root, name));
      if (loaded) return loaded;
    }
    return null;
  }
}

function readInstruction(filePath) {
  try {
    if (!fs.existsSync(filePath)) return null;
    const stat = fs.statSync(filePath);
    if (!stat.isFile()) return null;
    return {
      path: path.resolve(filePath),
      content: fs.readFileSync(filePath, 'utf8'),
    };
  } catch {
    return null;
  }
}

function dedupeInstructions(instructions) {
  const seen = new Set();
  const out = [];
  for (const item of instructions || []) {
    if (!item?.path || seen.has(item.path)) continue;
    seen.add(item.path);
    out.push(item);
  }
  return out;
}

function getLoadedSet(scope) {
  if (!_loadedByScope.has(scope)) _loadedByScope.set(scope, new Set());
  return _loadedByScope.get(scope);
}

function isWithin(root, target) {
  return target === root || target.startsWith(root + path.sep);
}

function realpathBestEffort(filePath) {
  try {
    return fs.realpathSync(filePath);
  } catch {
    return path.resolve(filePath);
  }
}

function expandGlob(pattern) {
  const absolutePattern = path.resolve(pattern);
  const firstGlob = absolutePattern.search(/[*?]/);
  const base = firstGlob === -1
    ? path.dirname(absolutePattern)
    : nearestExistingParent(absolutePattern.slice(0, firstGlob));
  const regex = globToRegExp(absolutePattern);
  const matches = [];
  walkFiles(base, (filePath) => {
    if (regex.test(path.resolve(filePath))) matches.push(filePath);
  });
  return matches;
}

function nearestExistingParent(prefix) {
  let dir = prefix.endsWith(path.sep) ? prefix.slice(0, -1) : path.dirname(prefix);
  while (dir && dir !== path.dirname(dir)) {
    if (fs.existsSync(dir)) return dir;
    dir = path.dirname(dir);
  }
  return fs.existsSync(dir) ? dir : process.cwd();
}

function walkFiles(dir, visit) {
  let entries;
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (entry.name === 'node_modules' || entry.name === '.git') continue;
      walkFiles(full, visit);
    } else if (entry.isFile()) {
      visit(full);
    }
  }
}

function globToRegExp(pattern) {
  const escaped = path.resolve(pattern)
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
    .replace(/\*\*/g, '<<GLOBSTAR>>')
    .replace(/\*/g, '[^/]*')
    .replace(/\?/g, '[^/]')
    .replace(/<<GLOBSTAR>>/g, '.*');
  return new RegExp(`^${escaped}$`);
}

function resetInstructionDedupe() {
  _loadedByScope.clear();
}

module.exports = {
  InstructionService,
  DEFAULT_INSTRUCTION_FILES,
  DEFAULT_TIMEOUT_MS,
  resetInstructionDedupe,
  expandGlob,
};
