// inquiryLine.ts
// inquiryLine owns read-only LineOut inspection requests and generated prj-map updates.
// It does not own recorder writes, success/fail notices, or message waiting state.

import fs from "node:fs";
import path from "node:path";
import { controller } from "./controller";
import { capture } from "./capture";
import { ROOT, SYSTEM_PROJECT_META_PATH, isInsideRoot } from "./pathList";
import {
  type MapSchema,
  type TraceHit,
  isInquiryRequest,
  normalizeInquiryName
} from "./sysutil/projectInquiries";

function existsFile(filePath: string): boolean {
  try { return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); }
  catch { return false; }
}

function existsDir(filePath: string): boolean {
  try { return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory(); }
  catch { return false; }
}

function readJsonFile(filePath: string, fallback: any = {}) {
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
  catch { return fallback; }
}

function writeJsonFile(filePath: string, value: unknown): void {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
}

function fail(reply: any, code: number, error: string, message?: string, extra: Record<string, any> = {}) {
  const body = { ok: false, error, ...(message ? { message } : {}), ...extra };
  return reply?.code ? reply.code(code).send(body) : body;
}

function activeScope() {
  const active = controller.activeProjectInquiry();
  if (!active.ok) throw new Error(active.error || "project_required_for_inquiry");
  const root = path.resolve(active.projectRoot || ROOT);
  return {
    ...active,
    root,
    metaPath: active.isBackstage ? SYSTEM_PROJECT_META_PATH : path.join(root, "project.json")
  };
}

function relFromScope(absPath: string, root: string): string {
  return path.relative(root, absPath).replace(/\\/g, "/") || ".";
}

function stripComment(line: string): string {
  const clean = String(line || "").trim();
  if (clean.startsWith("//")) return clean.replace(/^\/\/\s?/, "").trim();
  if (clean.startsWith("#")) return clean.replace(/^#\s?/, "").trim();
  if (clean.startsWith("/*") && clean.endsWith("*/")) return clean.replace(/^\/\*\s?/, "").replace(/\s?\*\/$/, "").trim();
  if (clean.startsWith("<!--") && clean.endsWith("-->")) return clean.replace(/^<!--\s?/, "").replace(/\s?-->$/, "").trim();
  return "";
}

function isCommentLine(line: string): boolean {
  return Boolean(stripComment(line));
}

function secondLineScopeDescriptor(filePath: string): string {
  try {
    const lines = fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n").split("\n");
    if (lines.length < 2 || !isCommentLine(lines[1])) return "";
    return stripComment(lines[1]);
  } catch {
    return "";
  }
}

const MAP_TEXT_FILE_EXTENSIONS = new Set([
  ".ts",
  ".js",
  ".css",
  ".html",
  ".json",
  ".md",
  ".txt",
  ".bat",
  ".mjs",
  ".cjs"
]);

const MAP_FILE_EXTENSIONS = new Set([
  ".ts",
  ".js",
  ".css",
  ".html",
  ".json",
  ".md",
  ".txt",
  ".bat",
  ".mjs",
  ".cjs",
  ".png",
  ".jpg",
  ".jpeg",
  ".gif",
  ".ico",
  ".webp",
  ".bmp",
  ".svg",
  ".ttf",
  ".woff",
  ".woff2"
]);

const MAP_SKIP_FOLDERS = new Set([
  "node_modules",
  ".git",
  "tmp",
  ".temp",
  "logs",
  "scrapes",
  "backups",
  "assets",
  "user",
  "users"
]);

function shouldMapFile(name: string): boolean {
  return MAP_FILE_EXTENSIONS.has(path.extname(name).toLowerCase());
}

function existingMapDescriptions(prjMap: unknown): Map<string, string> {
  const descriptions = new Map<string, string>();
  if (!Array.isArray(prjMap)) return descriptions;
  for (const item of prjMap) {
    const filePath = String((item as any)?.FilePath || (item as any)?.location || "").trim();
    const desc = String((item as any)?.desc || "").trim();
    if (filePath && desc) descriptions.set(filePath, desc);
  }
  return descriptions;
}

function mapEntry(FilePath: string, desc = ""): MapSchema {
  return desc ? { FilePath, desc } : { FilePath };
}
function shouldReadScopeLine(name: string): boolean {
  return MAP_TEXT_FILE_EXTENSIONS.has(path.extname(name).toLowerCase());
}

function fileScopeDescription(filePath: string, name: string, preserved: Map<string, string>, FilePath: string): string {
  if (!shouldReadScopeLine(name)) return preserved.get(FilePath) || "";
  return secondLineScopeDescriptor(filePath) || preserved.get(FilePath) || "";
}

function folderScopeDescription(FilePath: string, preserved: Map<string, string>): string {
  return preserved.get(FilePath) || "[folder]";
}

function buildProjectMap(root: string, preserved = new Map<string, string>()): MapSchema[] {
  const out: MapSchema[] = [];
  const walk = (dir: string) => {
    let entries: fs.Dirent[] = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
    for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
      if (entry.name.startsWith(".") && !MAP_SKIP_FOLDERS.has(entry.name)) continue;
      const abs = path.join(dir, entry.name);
      const FilePath = relFromScope(abs, root);
      if (entry.isDirectory()) {
        if (MAP_SKIP_FOLDERS.has(entry.name)) continue;
        out.push(mapEntry(FilePath, folderScopeDescription(FilePath, preserved)));
        walk(abs);
        continue;
      }
      if (!entry.isFile() || !shouldMapFile(entry.name)) continue;
      out.push(mapEntry(FilePath, fileScopeDescription(abs, entry.name, preserved, FilePath)));
    }
  };
  walk(root);
  return out;
}
function saveProjectMapToProject(prjMap: MapSchema[], generatedAt: string): any {
  const scope = activeScope();
  const doc = readJsonFile(scope.metaPath, {});
  const next = { ...doc, prjMap, prjMapUpdatedAt: generatedAt };
  writeJsonFile(scope.metaPath, next);
  return next;
}

function intentSections(doc: any): any[] {
  const sections = Array.isArray(doc?.sections) ? doc.sections : [];
  return sections.filter((section: any) => Boolean(section?.intentRepo));
}
function projectInformation() {
  const scope = activeScope();
  const doc = readJsonFile(scope.metaPath, {});
  return {
    ok: true,
    channel: "inquiry",
    request: "getprojectinfo",
    project: {
      name: String(doc.name || doc.details?.name || scope.projectName || scope.activeProject || "").trim(),
      serialNo: String(doc.serialNo || doc.details?.serialNo || scope.projectSerial || "").trim(),
      summary: String(doc.summary || doc.details?.summary || "").trim(),
      notes: String(doc.notes || doc.details?.notes || "").trim(),
      intent: intentSections(doc),
      prjMap: Array.isArray(doc.prjMap) ? doc.prjMap : [],
      prjMapUpdatedAt: doc.prjMapUpdatedAt || null
    }
  };
}
function fileTree() {
  const scope = activeScope();
  const doc = readJsonFile(scope.metaPath, {});

  let prjMap = Array.isArray(doc.prjMap) ? doc.prjMap : [];
  let prjMapUpdatedAt = doc.prjMapUpdatedAt || null;

  if (!prjMap.length) {
    prjMap = buildProjectMap(scope.root, existingMapDescriptions(doc.prjMap));
    prjMapUpdatedAt = new Date().toISOString();

    writeJsonFile(scope.metaPath, {
      ...doc,
      prjMap,
      prjMapUpdatedAt
    });
  }

  return {
    ok: true,
    channel: "inquiry",
    request: "getfiletree",
    prjMap,
    prjMapUpdatedAt
  };
}

function projectMap() {
  const scope = activeScope();
  const doc = readJsonFile(scope.metaPath, {});
  return {
    ok: true,
    channel: "inquiry",
    request: "prj-map",
    prjMap: Array.isArray(doc.prjMap) ? doc.prjMap : [],
    prjMapUpdatedAt: doc.prjMapUpdatedAt || null
  };
}

export function updateProjectMap() {
  const scope = activeScope();
  const doc = readJsonFile(scope.metaPath, {});
  const map = buildProjectMap(scope.root, existingMapDescriptions(doc.prjMap));
  const generatedAt = new Date().toISOString();
  saveProjectMapToProject(map, generatedAt);
  return { ok: true, channel: "inquiry", request: "update.prj-map", prjMap: map, prjMapUpdatedAt: generatedAt };
}

function findUniqueFileByName(root: string, requested: string): string | null {
  const wanted = path.basename(String(requested || "").replace(/\\/g, "/")).toLowerCase();
  if (!wanted) return null;

  const matches: string[] = [];
  const walk = (dir: string) => {
    if (matches.length > 1) return;
    let entries: fs.Dirent[] = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }

    for (const entry of entries) {
      if (matches.length > 1) return;
      if (entry.name.startsWith(".") && !MAP_SKIP_FOLDERS.has(entry.name)) continue;
      const abs = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (!MAP_SKIP_FOLDERS.has(entry.name)) walk(abs);
        continue;
      }
      if (entry.isFile() && entry.name.toLowerCase() === wanted) matches.push(abs);
    }
  };

  walk(root);
  return matches.length === 1 ? matches[0] : null;
}

function resolveProjectFileWithFallback(FilePath: string, scope: ReturnType<typeof activeScope>): { abs: string; FilePath: string; resolvedFrom?: string } {
  try {
    return { abs: controller.resolveProjectPath(FilePath), FilePath };
  } catch (err: any) {
    const message = String(err?.message || err || "");
    if (!message.toLowerCase().includes("not_found") && !message.toLowerCase().includes("enoent")) throw err;
  }

  const found = findUniqueFileByName(scope.root, FilePath);
  if (!found) throw new Error("file_not_found");
  return { abs: found, FilePath: relFromScope(found, scope.root), resolvedFrom: FilePath };
}

function readProjectFiles(body: Record<string, any>, reply: any) {
  const rawPaths = Array.isArray(body.paths) ? body.paths : [body.target || body.FilePath || body.filePath || body.path].filter(Boolean);
  const paths = rawPaths.map(String).map(s => s.trim()).filter(Boolean);
  if (!paths.length) return fail(reply, 400, "missing_project_file", "prj-file requires FilePath or paths.");

  const scope = activeScope();

  return {
    ok: true,
    channel: "inquiry",
    request: "getfileinfo",
    count: paths.length,
    files: paths.map((requestedPath: string) => {
      try {
        const resolved = resolveProjectFileWithFallback(requestedPath, scope);
        const abs = resolved.abs;
        const FilePath = resolved.FilePath;
        if (existsFile(abs)) return { FilePath, requestedPath, resolvedFrom: resolved.resolvedFrom, ok: true, type: "file", content: fs.readFileSync(abs, "utf8") };
        if (existsDir(abs)) {
          const entries = fs.readdirSync(abs, { withFileTypes: true })
            .filter(entry => !entry.name.startsWith(".") && !MAP_SKIP_FOLDERS.has(entry.name))
            .sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
            .map(entry => ({ name: entry.name, type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other", path: path.join(FilePath, entry.name).replace(/\\/g, "/") }));
          return { FilePath, requestedPath, resolvedFrom: resolved.resolvedFrom, ok: true, type: "directory", entries, content: entries.map(entry => `${entry.type}\t${entry.path}`).join("\n") };
        }
        return { FilePath: requestedPath, ok: false, error: "file_not_found" };
      } catch (err: any) {
        return { FilePath: requestedPath, ok: false, error: err?.message || String(err) };
      }
    })
  };
}
function recordingContract(body: Record<string, any>, reply: any) {
  const result: any = readProjectFiles({ ...body, FilePath: "docs/VerbatimRecordingContract.txt" }, reply);
  if (result && typeof result === "object") result.request = "recording-contract";
  return result;
}

function traceableFiles(root: string, limit = 240): string[] {
  const out: string[] = [];
  const walk = (dir: string) => {
    if (out.length >= limit) return;
    let entries: fs.Dirent[] = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
    for (const entry of entries) {
      if (out.length >= limit) return;
      if (entry.name.startsWith(".") && !MAP_SKIP_FOLDERS.has(entry.name)) continue;
      const abs = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (!MAP_SKIP_FOLDERS.has(entry.name)) walk(abs);
        continue;
      }
      if (entry.isFile() && shouldReadScopeLine(entry.name)) out.push(abs);
    }
  };
  walk(root);
  return out;
}
function searchFileNames(root: string, term: string, limit = 200): string[] {
  const wanted = term.toLowerCase();
  const matches: string[] = [];
  const walk = (dir: string) => {
    if (matches.length >= limit) return;
    let entries: fs.Dirent[] = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
    for (const entry of entries) {
      if (matches.length >= limit) return;
      if (entry.name.startsWith(".") && !MAP_SKIP_FOLDERS.has(entry.name)) continue;
      const abs = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (!MAP_SKIP_FOLDERS.has(entry.name)) walk(abs);
        continue;
      }
      if (entry.isFile() && entry.name.toLowerCase().includes(wanted)) {
        matches.push(relFromScope(abs, root));
      }
    }
  };
  walk(root);
  return matches;
}

function searchTerm(body: Record<string, any>, reply: any) {
  const term = String(body.term || body.query || body.name || "").trim();
  if (!term) return fail(reply, 400, "missing_search_term", "search.term requires term.");

  const scope = activeScope();
  const limit = Math.max(1, Math.min(500, Number(body.limit || 200)));
  const matches = searchFileNames(scope.root, term, limit);

  return { ok: true, channel: "inquiry", request: "search.term", term, count: matches.length, files: matches };
}

function traceTerm(body: Record<string, any>, reply: any) {
  const term = String(body.term || body.query || body.name || "").trim();
  if (!term) return fail(reply, 400, "missing_trace_term", "trace.term requires term.");

  const scope = activeScope();
  const limit = Math.max(1, Math.min(500, Number(body.limit || 200)));
  const files = traceableFiles(scope.root, limit);
  const hits: any[] = [];

  for (const abs of files) {
    let content = "";
    try {
      content = fs.readFileSync(abs, "utf8");
    } catch {
      continue;
    }

    const lines = content.replace(/\r\n/g, "\n").split("\n");
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].toLowerCase().includes(term.toLowerCase())) {
        hits.push({
          FilePath: relFromScope(abs, scope.root),
          line: i + 1,
          text: lines[i].trim()
        });
      }
    }
  }

  return {
    ok: true,
    channel: "inquiry",
    request: "trace.term",
    term,
    count: hits.length,
    hits
  };
}

function inquiryTarget(body: Record<string, any>): string {
  return String(body.target || body.FilePath || body.filePath || body.path || body.term || body.query || body.name || "").trim();
}
export async function handleInquiry(body: Record<string, any> = {}, reply?: any) {
  try {
    const request = normalizeInquiryName(body.request || body.Request || "getprojectinfo");

    switch (request) {
      case "getprojectinfo":
        return projectInformation();

      case "getfiletree":
        return fileTree();

      case "getfileinfo":
        return readProjectFiles(body, reply);

      case "trace.term":
        return traceTerm(body, reply);

      case "search.term":
        return searchTerm(body, reply);

      case "recording-contract":
        return recordingContract(body, reply);

      default:
        return fail(reply, 400, "unknown_inquiry_request", `Unknown inquiry request: ${request}`);
    }
  } catch (err: any) {
    return fail(reply, 400, "inquiry_failed", err?.message || String(err));
  }
}