const fs = require('fs');
const path = require('path');
const { getConfig } = require('../configs/config-manager.js');

const ROOT = path.resolve(__dirname, '..', '..');

const ALLOWED_ROOTS = ['src', 'docs', 'scripts', 'test'];
const ALLOWED_ROOT_FILES = ['README.md', 'LICENSE', 'install.sh', 'oasis.sh'];
const DENIED_SEGMENTS = new Set(['node_modules', '.git', '.ssb', 'packages', 'configs', 'embeddings', 'tiles', 'cache', 'results']);
const DENIED_NAMES = new Set(['secret', '.env', '.npmrc', 'package-lock.json']);
const DENIED_EXTENSIONS = new Set(['.gguf', '.jks', '.keystore', '.enc', '.pem', '.key', '.p12', '.crt']);
const VIEWABLE_EXTENSIONS = new Set(['.js', '.mjs', '.json', '.css', '.md', '.sh', '.html', '.txt', '.yml', '.yaml']);

const STATS_TTL = 60 * 1000;
let statsCache = null;
let statsCacheAt = 0;

const MAX_VIEW_BYTES = 2 * 1024 * 1024;
const PAGE_LINES = 500;
const SEARCH_MAX = 200;
const SEARCH_SNIPPET = 200;

const extOf = (name) => {
  const i = String(name || '').lastIndexOf('.');
  return i > 0 ? String(name).slice(i).toLowerCase() : '';
};

const isDeniedEntry = (name) => {
  const n = String(name || '');
  if (!n || n === '.' || n === '..') return true;
  if (n.toLowerCase().startsWith('.git')) return true;
  if (DENIED_SEGMENTS.has(n)) return true;
  if (DENIED_NAMES.has(n)) return true;
  if (DENIED_EXTENSIONS.has(extOf(n))) return true;
  return false;
};

const PLAIN_TEXT_NAME = /^[A-Z][A-Z0-9_-]*$/;

const isViewable = (name) => (VIEWABLE_EXTENSIONS.has(extOf(name)) || PLAIN_TEXT_NAME.test(String(name || ''))) && !isDeniedEntry(name);

function realRoot() {
  try { return fs.realpathSync(ROOT); } catch (_) { return ROOT; }
}

function splitPath(rel) {
  const raw = String(rel || '');
  for (let i = 0; i < raw.length; i++) { if (raw.charCodeAt(i) < 32) throw new Error('Invalid path'); }
  return raw.replace(/^[/\\]+/, '').split(/[/\\]+/).filter(s => s && s !== '.');
}

function resolveSafe(rel) {
  const segments = splitPath(rel);
  if (segments.some(s => s === '..')) throw new Error('Invalid path');
  if (segments.some(isDeniedEntry)) throw new Error('Path not available');
  if (segments.length) {
    const head = segments[0];
    const isRootFile = segments.length === 1 && ALLOWED_ROOT_FILES.includes(head);
    if (!ALLOWED_ROOTS.includes(head) && !isRootFile) throw new Error('Path not available');
  }
  const target = path.resolve(ROOT, segments.join(path.sep));
  let real;
  try { real = fs.realpathSync(target); } catch (_) { throw new Error('Not found'); }
  const base = realRoot();
  if (real !== base && !real.startsWith(base + path.sep)) throw new Error('Path not available');
  return { abs: real, rel: segments.join('/'), segments };
}

function parentsOf(segments) {
  const out = [];
  let acc = '';
  for (const s of segments) {
    acc = acc ? `${acc}/${s}` : s;
    out.push({ name: s, path: acc });
  }
  return out;
}

function rootListing() {
  const dirs = [];
  const files = [];
  for (const name of ALLOWED_ROOTS) {
    const abs = path.join(ROOT, name);
    if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) dirs.push({ name, path: name });
  }
  for (const name of ALLOWED_ROOT_FILES) {
    const abs = path.join(ROOT, name);
    if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
    files.push({ name, path: name, size: fs.statSync(abs).size, ext: extOf(name), viewable: isViewable(name) });
  }
  return { path: '', parents: [], dirs, files, hidden: 0 };
}

function walkFiles(relDir, onFile) {
  let entries;
  try { entries = fs.readdirSync(path.resolve(ROOT, relDir), { withFileTypes: true }); } catch (_) { return true; }
  for (const e of entries) {
    if (isDeniedEntry(e.name)) continue;
    const rel = relDir ? `${relDir}/${e.name}` : e.name;
    if (e.isSymbolicLink()) continue;
    if (e.isDirectory()) { if (walkFiles(rel, onFile) === false) return false; continue; }
    if (!e.isFile()) continue;
    if (onFile(rel, e.name) === false) return false;
  }
  return true;
}

const MODULE_ALIASES = {
  ai: { model: 'src/AI/ai_service.mjs', view: 'src/views/AI_view.js' },
  aiNav: { model: 'src/AI/routes_index.js', view: 'src/views/AI_view.js', paths: ['/ai/ask'] },
  docs: { paths: ['/documents'] },
  latest: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/latest'] },
  popular: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/popular'] },
  topics: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/latest/topics'] },
  summaries: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/latest/summaries'] },
  threads: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/latest/threads'] },
  multiverse: { model: 'src/models/main_models.js', view: 'src/views/main_views.js', paths: ['/public/latest/extended'] },
  invites: { model: 'src/models/main_models.js', view: 'src/views/invites_view.js' },
  graphos: { model: 'src/models/main_models.js', view: 'src/views/graphos_view.js' }
};

const GROUPS = {
  translations: ['src/client/assets/translations'],
  styles: ['src/client/assets/styles', 'src/client/assets/themes'],
  tests: ['test']
};

module.exports = {
  ROOT,
  PAGE_LINES,
  GROUPS,

  listGroup(key) {
    const roots = GROUPS[String(key || '')];
    if (!roots) throw new Error('Not found');
    const dirs = [];
    const files = [];
    for (const rel of roots) {
      let listing;
      try { listing = module.exports.listTree(rel); } catch (_) { continue; }
      dirs.push(...listing.dirs);
      files.push(...listing.files);
    }
    dirs.sort((a, b) => a.name.localeCompare(b.name));
    files.sort((a, b) => a.name.localeCompare(b.name));
    return { group: String(key), path: '', parents: [], dirs, files, hidden: 0 };
  },

  listTree(relPath) {
    const segments = splitPath(relPath);
    if (!segments.length) return rootListing();
    const target = resolveSafe(relPath);
    const stat = fs.statSync(target.abs);
    if (!stat.isDirectory()) throw new Error('Not a directory');
    const entries = fs.readdirSync(target.abs, { withFileTypes: true });
    const dirs = [];
    const files = [];
    let hidden = 0;
    for (const e of entries) {
      if (isDeniedEntry(e.name) || e.isSymbolicLink()) { hidden++; continue; }
      const rel = `${target.rel}/${e.name}`;
      if (e.isDirectory()) { dirs.push({ name: e.name, path: rel }); continue; }
      if (!e.isFile()) { hidden++; continue; }
      let size = 0;
      try { size = fs.statSync(path.join(target.abs, e.name)).size; } catch (_) { size = 0; }
      files.push({ name: e.name, path: rel, size, ext: extOf(e.name), viewable: isViewable(e.name) });
    }
    dirs.sort((a, b) => a.name.localeCompare(b.name));
    files.sort((a, b) => a.name.localeCompare(b.name));
    return { path: target.rel, parents: parentsOf(target.segments), dirs, files, hidden };
  },

  readFile(relPath, opts = {}) {
    const target = resolveSafe(relPath);
    const stat = fs.statSync(target.abs);
    if (!stat.isFile()) throw new Error('Not a file');
    if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed');
    if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed');
    const all = fs.readFileSync(target.abs, 'utf8').split('\n');
    const totalLines = all.length;
    const requested = parseInt(opts.from, 10);
    const from = Math.min(Math.max(Number.isFinite(requested) ? requested : 1, 1), Math.max(1, totalLines));
    const to = Math.min(from + PAGE_LINES - 1, totalLines);
    const lines = [];
    for (let n = from; n <= to; n++) lines.push({ n, text: all[n - 1] });
    return {
      path: target.rel,
      parents: parentsOf(target.segments),
      name: path.basename(target.abs),
      size: stat.size,
      totalLines,
      from,
      to,
      pageLines: PAGE_LINES,
      hasPrev: from > 1,
      hasNext: to < totalLines,
      lines
    };
  },

  rawFile(relPath) {
    const target = resolveSafe(relPath);
    const stat = fs.statSync(target.abs);
    if (!stat.isFile()) throw new Error('Not a file');
    if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed');
    if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed');
    return { name: path.basename(target.abs), content: fs.readFileSync(target.abs, 'utf8') };
  },

  searchCode(query, opts = {}) {
    const q = String(query || '').trim();
    if (q.length < 2) return { query: q, results: [], truncated: false, scanned: 0 };
    const wanted = String(opts.ext || '').trim().toLowerCase();
    const max = Math.min(Math.max(parseInt(opts.max, 10) || SEARCH_MAX, 1), SEARCH_MAX);
    const needle = q.toLowerCase();
    const results = [];
    let scanned = 0;
    let truncated = false;
    for (const root of ALLOWED_ROOTS) {
      if (truncated) break;
      walkFiles(root, (rel, name) => {
        if (!isViewable(name)) return true;
        if (wanted && extOf(name) !== wanted) return true;
        let stat;
        try { stat = fs.statSync(path.resolve(ROOT, rel)); } catch (_) { return true; }
        if (stat.size > MAX_VIEW_BYTES) return true;
        scanned++;
        let content;
        try { content = fs.readFileSync(path.resolve(ROOT, rel), 'utf8'); } catch (_) { return true; }
        if (content.toLowerCase().indexOf(needle) < 0) return true;
        const lines = content.split('\n');
        for (let i = 0; i < lines.length; i++) {
          if (lines[i].toLowerCase().indexOf(needle) < 0) continue;
          results.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, SEARCH_SNIPPET) });
          if (results.length >= max) { truncated = true; return false; }
        }
        return true;
      });
    }
    return { query: q, results, truncated, scanned };
  },

  moduleMap() {
    const cfg = getConfig();
    const mods = Object.keys((cfg && cfg.modules) || {}).map(k => k.replace(/Mod$/, '')).sort();
    let backend = '';
    try { backend = fs.readFileSync(path.join(ROOT, 'src', 'backend', 'backend.js'), 'utf8'); } catch (_) { backend = ''; }
    const modelFiles = fs.existsSync(path.join(ROOT, 'src', 'models')) ? fs.readdirSync(path.join(ROOT, 'src', 'models')) : [];
    const viewFiles = fs.existsSync(path.join(ROOT, 'src', 'views')) ? fs.readdirSync(path.join(ROOT, 'src', 'views')) : [];
    const stem = (s) => String(s || '').toLowerCase().replace(/s$/, '');
    const pick = (files, name, suffixes) => {
      for (const suf of suffixes) {
        const exact = files.find(f => f.toLowerCase() === `${name.toLowerCase()}${suf}`);
        if (exact) return exact;
      }
      const singular = name.replace(/s$/, '');
      for (const suf of suffixes) {
        const short = files.find(f => f.toLowerCase() === `${singular.toLowerCase()}${suf}`);
        if (short) return short;
      }
      const target = stem(name);
      return files.find(f => {
        const suf = suffixes.find(x => f.toLowerCase().endsWith(x));
        if (!suf) return false;
        const base = stem(f.slice(0, f.length - suf.length));
        return base.startsWith(target) || target.startsWith(base);
      }) || null;
    };
    const testsIndex = new Map();
    const modsDir = path.join(ROOT, 'test', 'mods');
    const indexTests = (dir, prefix) => {
      let entries;
      try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
      for (const e of entries) {
        if (!e.isDirectory()) continue;
        const rel = `${prefix}/${e.name}`;
        if (!testsIndex.has(e.name)) testsIndex.set(e.name, rel);
        indexTests(path.join(dir, e.name), rel);
      }
    };
    indexTests(modsDir, 'test/mods');
    return mods.map(name => {
      const alias = MODULE_ALIASES[name] || {};
      const routePaths = Array.isArray(alias.paths) && alias.paths.length ? alias.paths : [`/${name}`];
      let routes = 0;
      for (const routePath of routePaths) {
        const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const re = new RegExp(`\\.(get|post)\\(\\s*["'\`]${escaped}(?=["'/?])`, 'g');
        routes += backend ? (backend.match(re) || []).length : 0;
      }
      const model = alias.model !== undefined ? alias.model : (pick(modelFiles, name, ['_model.js', '_models.js']) ? `src/models/${pick(modelFiles, name, ['_model.js', '_models.js'])}` : null);
      const view = alias.view !== undefined ? alias.view : (pick(viewFiles, name, ['_view.js', '_views.js']) ? `src/views/${pick(viewFiles, name, ['_view.js', '_views.js'])}` : null);
      return {
        name,
        enabled: (cfg.modules || {})[`${name}Mod`] !== 'off',
        model,
        view,
        routes,
        tests: testsIndex.get(name) || testsIndex.get(name.replace(/s$/, '')) || (() => {
          const target = stem(name);
          for (const [dir, rel] of testsIndex) {
            const base = stem(dir);
            if (base.startsWith(target) || target.startsWith(base)) return rel;
          }
          return null;
        })()
      };
    });
  },

  projectStats() {
    const now = Date.now();
    if (statsCache && now - statsCacheAt < STATS_TTL) return statsCache;
    let files = 0;
    let lines = 0;
    let bytes = 0;
    for (const root of ALLOWED_ROOTS) {
      walkFiles(root, (rel, name) => {
        if (!isViewable(name)) return true;
        try {
          const abs = path.resolve(ROOT, rel);
          const stat = fs.statSync(abs);
          if (stat.size > MAX_VIEW_BYTES) return true;
          files++;
          bytes += stat.size;
          lines += fs.readFileSync(abs, 'utf8').split('\n').length;
        } catch (_) {}
        return true;
      });
    }
    const cfg = getConfig();
    const modules = Object.keys((cfg && cfg.modules) || {}).length;
    const translations = path.join(ROOT, 'src', 'client', 'assets', 'translations');
    const languages = fs.existsSync(translations) ? fs.readdirSync(translations).filter(f => /^oasis_[a-z]{2}\.js$/.test(f)).length : 0;
    const testsDir = path.join(ROOT, 'test', 'mods');
    const testMods = fs.existsSync(testsDir) ? fs.readdirSync(testsDir).length : 0;
    let version = '';
    try { version = String(JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'server', 'package.json'), 'utf8')).version || ''); } catch (_) { version = ''; }
    statsCache = { files, lines, bytes, modules, languages, testMods, version, node: process.version };
    statsCacheAt = now;
    return statsCache;
  }
};
