/**
 * project-inventory/execute.ts — Scan the project and build the JSON report.
 *
 * Regex-first, AST-free. Keeps the CLI zero-dep (besides Zod) and fast.
 * False positives are acceptable — they surface to the subagent as data to
 * inspect, not as truth. False negatives are worse (they'd hide existing work)
 * so we err on the side of matching too generously.
 */
import path from 'node:path';
import { findFiles, readText, directoryExists } from '../../../../lib/fs.js';
import type {
  ControllerInfo,
  DomainEntity,
  InventoryDomain,
  InventoryReport,
  MigrationInfo,
  PageInfo,
  ProjectInventoryArgs,
  SeedProviderInfo,
} from './types.js';

// ─── Path helpers ─────────────────────────────────────────────────────────

function rel(projectPath: string, abs: string): string {
  return path.relative(projectPath, abs).replace(/\\/g, '/');
}

// ─── Layer detection (cheap — only two probes) ────────────────────────────

async function hasDotnetRoot(projectPath: string): Promise<boolean> {
  const probes = [projectPath, path.join(projectPath, 'src'), path.join(projectPath, 'dotnet')];
  for (const p of probes) {
    if (!(await directoryExists(p))) continue;
    const csproj = await findFiles('**/*.csproj', { cwd: p });
    if (csproj.length > 0) return true;
  }
  return false;
}

async function hasFrontendRoot(projectPath: string): Promise<boolean> {
  const probes = [
    path.join(projectPath, 'web'),
    path.join(projectPath, 'src', 'frontend'),
    projectPath,
  ];
  for (const p of probes) {
    if (!(await directoryExists(p))) continue;
    const pkg = await findFiles('**/package.json', {
      cwd: p,
      ignore: ['**/node_modules/**'],
    });
    if (pkg.length > 0) return true;
  }
  return false;
}

// ─── Domain entities ──────────────────────────────────────────────────────

const ENTITY_BASES = new Set([
  'BaseEntity',
  'AuditableEntity',
  'TenantEntity',
  'ITenantEntity',
  'Entity',
]);

/**
 * Matches class declarations whose direct base type looks like an entity root.
 * `class Foo : BaseEntity, IFoo { ... }` — captures `Foo` and `BaseEntity`.
 * Ignores abstract helpers and generic dictionary classes.
 */
const ENTITY_DECL_RE = /(?:^|\n)\s*(?:public\s+)?(?:sealed\s+|abstract\s+)?class\s+([A-Z]\w+)\s*:\s*([A-Za-z_][\w.<>]*)/g;
const NAMESPACE_RE = /namespace\s+([A-Za-z_][\w.]*)\s*[{;]/;

async function scanEntities(projectPath: string): Promise<DomainEntity[]> {
  // Match any path segment that ends with `Domain` (e.g. `SmartStack.Domain`, `TestV2.Domain`, bare `Domain`).
  const domainFiles = [
    ...(await findFiles('**/Domain/**/*.cs', { cwd: projectPath })),
    ...(await findFiles('**/*Domain/**/*.cs', { cwd: projectPath })),
  ];
  // Dedup and exclude files inside `Migrations/` or `Seeding/` subfolders — those are not domain.
  const seen = new Set<string>();
  const filtered = domainFiles.filter((f) => {
    if (seen.has(f)) return false;
    seen.add(f);
    const norm = f.replace(/\\/g, '/');
    return !/\/Migrations\//i.test(norm) && !/\/Seeding\//i.test(norm);
  });
  const results: DomainEntity[] = [];
  for (const file of filtered) {
    if (file.endsWith('.Designer.cs') || file.endsWith('.g.cs')) continue;
    let content: string;
    try {
      content = await readText(file);
    } catch {
      continue;
    }
    const ns = NAMESPACE_RE.exec(content)?.[1] ?? null;
    ENTITY_DECL_RE.lastIndex = 0;
    let m: RegExpExecArray | null;
    while ((m = ENTITY_DECL_RE.exec(content)) !== null) {
      const className = m[1];
      const baseClass = m[2].split('<')[0]; // drop generic args for the set check
      if (!ENTITY_BASES.has(baseClass)) continue;
      results.push({
        name: className,
        file: rel(projectPath, file),
        baseClass,
        namespace: ns,
      });
    }
  }
  // Dedup by (name, file) — a single file might match twice if two matches overlap.
  const key = (e: DomainEntity) => `${e.file}::${e.name}`;
  return [...new Map(results.map((r) => [key(r), r])).values()];
}

// ─── Migrations ───────────────────────────────────────────────────────────

const MIGRATION_CLASS_RE = /public\s+partial\s+class\s+([A-Za-z_]\w*)\s*:\s*Migration\b/;
const CREATE_TABLE_RE = /migrationBuilder\.CreateTable\(\s*name:\s*"([^"]+)"/g;
const DROP_TABLE_RE = /migrationBuilder\.DropTable\(\s*name:\s*"([^"]+)"/g;
const MIGRATION_FILE_RE = /^(\d{14,})_([A-Za-z0-9_]+)\.cs$/;

async function scanMigrations(projectPath: string): Promise<MigrationInfo[]> {
  const migFiles = await findFiles('**/Migrations/*.cs', { cwd: projectPath });
  const results: MigrationInfo[] = [];
  for (const file of migFiles) {
    const base = path.basename(file);
    if (base.endsWith('.Designer.cs') || base.endsWith('ModelSnapshot.cs')) continue;
    let content: string;
    try {
      content = await readText(file);
    } catch {
      continue;
    }
    const classMatch = MIGRATION_CLASS_RE.exec(content);
    if (!classMatch) continue;
    const fileMatch = MIGRATION_FILE_RE.exec(base);
    const timestamp = fileMatch?.[1] ?? null;
    const name = fileMatch?.[2] ?? classMatch[1];
    const tablesCreated: string[] = [];
    const tablesDropped: string[] = [];
    CREATE_TABLE_RE.lastIndex = 0;
    let cm: RegExpExecArray | null;
    while ((cm = CREATE_TABLE_RE.exec(content)) !== null) tablesCreated.push(cm[1]);
    DROP_TABLE_RE.lastIndex = 0;
    let dm: RegExpExecArray | null;
    while ((dm = DROP_TABLE_RE.exec(content)) !== null) tablesDropped.push(dm[1]);
    const action: MigrationInfo['action'] =
      tablesCreated.length > 0 ? 'create'
      : tablesDropped.length > 0 ? 'drop'
      : /migrationBuilder\.(Alter|Add|Drop)/.test(content) ? 'alter'
      : 'unknown';
    results.push({
      file: rel(projectPath, file),
      name,
      timestamp,
      tablesCreated,
      tablesDropped,
      action,
    });
  }
  results.sort((a, b) => (a.timestamp ?? '').localeCompare(b.timestamp ?? ''));
  return results;
}

// ─── Seed providers ───────────────────────────────────────────────────────

const SEED_CLASS_RE = /(?:public\s+)?(?:sealed\s+|abstract\s+)?class\s+([A-Za-z_][\w]*)\s*(?::|\{)/;
const NAV_MENTION_RE = /\b(NavigationApplication|NavigationSection|NavigationModule|NavRoute)\b/;
const PERM_MENTION_RE = /\b(Permission\(|permissionCode\b|IPermissionSeed|IPermissionSeeder)/;

/** Entity mentions: `new X(`, `typeof(X)`, `nameof(X)`, `<X>`. X must be PascalCase, ≥2 chars. */
function extractEntityMentions(source: string): string[] {
  const out = new Set<string>();
  const patterns: RegExp[] = [
    /\bnew\s+([A-Z][A-Za-z0-9]+)\s*\(/g,
    /\btypeof\(\s*([A-Z][A-Za-z0-9]+)\s*\)/g,
    /\bnameof\(\s*([A-Z][A-Za-z0-9]+)\s*\)/g,
  ];
  for (const re of patterns) {
    let m: RegExpExecArray | null;
    while ((m = re.exec(source)) !== null) out.add(m[1]);
  }
  return [...out];
}

/** Table mentions: explicit string literals that look like snake_case or PascalCase table names in seed context. */
function extractTableMentions(source: string): string[] {
  const out = new Set<string>();
  const re = /"([A-Z][A-Za-z0-9]+s?)"/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(source)) !== null) {
    const name = m[1];
    // Filter obvious non-table tokens (roles, codes, etc.).
    if (/^(Admin|User|Owner|Editor|Viewer|Platform|Support|Api|System)$/.test(name)) continue;
    if (name.length < 3) continue;
    out.add(name);
  }
  return [...out];
}

async function scanSeedProviders(projectPath: string): Promise<SeedProviderInfo[]> {
  const seedCandidates = await findFiles('**/Seeding/**/*.cs', { cwd: projectPath });
  const filtered = seedCandidates.filter((f) => {
    const base = path.basename(f).toLowerCase();
    return (base.includes('seed') || base.includes('provider'))
      && !base.endsWith('.designer.cs');
  });
  const results: SeedProviderInfo[] = [];
  for (const file of filtered) {
    let content: string;
    try {
      content = await readText(file);
    } catch {
      continue;
    }
    const cls = SEED_CLASS_RE.exec(content);
    if (!cls) continue;
    const entitiesReferenced = extractEntityMentions(content);
    const tablesReferenced = extractTableMentions(content);
    results.push({
      file: rel(projectPath, file),
      className: cls[1],
      entitiesReferenced,
      tablesReferenced,
      seedsNavigation: NAV_MENTION_RE.test(content),
      seedsPermissions: PERM_MENTION_RE.test(content),
    });
  }
  return results;
}

// ─── Controllers ──────────────────────────────────────────────────────────

const CONTROLLER_CLASS_RE = /(?:public\s+)?(?:sealed\s+)?class\s+([A-Za-z_]\w*Controller)\s*(?::\s*([A-Za-z_][\w.<>]*))?/;
const ROUTE_ATTR_RE = /\[Route\(\s*"([^"]+)"\s*\)\]/;
const HTTP_ATTR_RE = /\[Http(Get|Post|Put|Delete|Patch|Options)\b/g;

async function scanControllers(projectPath: string): Promise<ControllerInfo[]> {
  const files = await findFiles('**/Controllers/**/*Controller.cs', { cwd: projectPath });
  const results: ControllerInfo[] = [];
  for (const file of files) {
    let content: string;
    try {
      content = await readText(file);
    } catch {
      continue;
    }
    const cls = CONTROLLER_CLASS_RE.exec(content);
    if (!cls) continue;
    const routeMatch = ROUTE_ATTR_RE.exec(content);
    const route = routeMatch ? routeMatch[1] : null;
    const httpActions = new Set<string>();
    HTTP_ATTR_RE.lastIndex = 0;
    let m: RegExpExecArray | null;
    while ((m = HTTP_ATTR_RE.exec(content)) !== null) httpActions.add(m[1].toUpperCase());
    results.push({
      file: rel(projectPath, file),
      className: cls[1],
      route,
      httpActions: [...httpActions],
    });
  }
  return results;
}

// ─── React pages + registries + i18n ──────────────────────────────────────

const PAGE_FILENAME_RE = /^([A-Z]\w+?)(List|Detail|Form|Dashboard|Create|Edit)Page\.tsx$/;

function kindFromSuffix(suffix: string): PageInfo['kind'] {
  const k = suffix.toLowerCase();
  if (k === 'list' || k === 'detail' || k === 'form' || k === 'dashboard' || k === 'create' || k === 'edit') return k;
  return 'other';
}

async function scanFrontendPages(projectPath: string): Promise<PageInfo[]> {
  const candidates = await findFiles('**/*Page.tsx', {
    cwd: projectPath,
    ignore: ['**/node_modules/**', '**/dist/**', '**/out/**'],
  });
  const results: PageInfo[] = [];
  for (const file of candidates) {
    const base = path.basename(file);
    const m = PAGE_FILENAME_RE.exec(base);
    if (!m) continue;
    const entityRaw = m[1];
    const entity = entityRaw.charAt(0).toLowerCase() + entityRaw.slice(1);
    results.push({
      file: rel(projectPath, file),
      entity,
      kind: kindFromSuffix(m[2]),
    });
  }
  return results;
}

async function scanExtensionRegistries(projectPath: string): Promise<string[]> {
  const files = await findFiles('**/extensions/**/*Registry*.ts', {
    cwd: projectPath,
    ignore: ['**/node_modules/**'],
  });
  const withPageRegistry: string[] = [];
  for (const file of files) {
    let content: string;
    try {
      content = await readText(file);
    } catch {
      continue;
    }
    if (/PageRegistry\.register\s*\(/.test(content)) {
      withPageRegistry.push(rel(projectPath, file));
    }
  }
  return withPageRegistry;
}

async function scanI18n(projectPath: string): Promise<{ locales: string[]; namespaces: string[] }> {
  const jsonFiles = await findFiles('**/i18n/**/*.json', {
    cwd: projectPath,
    ignore: ['**/node_modules/**'],
  });
  const locales = new Set<string>();
  const namespaces = new Set<string>();
  // Locale tokens: 2-char (fr) or xx-YY (fr-CH). Anything else is a container folder.
  const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
  for (const file of jsonFiles) {
    const relPath = rel(projectPath, file);
    const parts = relPath.split('/');
    const i18nIdx = parts.indexOf('i18n');
    if (i18nIdx === -1 || parts.length <= i18nIdx + 1) continue;
    // Walk forward from i18n/ until we hit a segment that looks like a locale.
    for (let i = i18nIdx + 1; i < parts.length - 1; i++) {
      if (LOCALE_RE.test(parts[i])) {
        locales.add(parts[i]);
        break;
      }
    }
    const baseName = path.basename(file, '.json');
    namespaces.add(baseName);
  }
  return { locales: [...locales].sort(), namespaces: [...namespaces].sort() };
}

// ─── Entry point ──────────────────────────────────────────────────────────

export async function execute(
  args: ProjectInventoryArgs,
): Promise<{ report: InventoryReport; durationMs: number; domainsRun: InventoryDomain[] }> {
  const start = Date.now();
  const projectPath = path.resolve(args.projectPath);

  const wantAll = args.domains.length === 0;
  const runDomain = wantAll || args.domains.includes('domain');
  const runData = wantAll || args.domains.includes('data');
  const runApi = wantAll || args.domains.includes('api');
  const runFrontend = wantAll || args.domains.includes('frontend');

  const [hasDotnet, hasFrontend] = await Promise.all([
    hasDotnetRoot(projectPath),
    hasFrontendRoot(projectPath),
  ]);

  const [entities, migrations, seedProviders, controllers, pages, registries, i18n] = await Promise.all([
    runDomain && hasDotnet ? scanEntities(projectPath) : Promise.resolve([]),
    runData && hasDotnet ? scanMigrations(projectPath) : Promise.resolve([]),
    runData && hasDotnet ? scanSeedProviders(projectPath) : Promise.resolve([]),
    runApi && hasDotnet ? scanControllers(projectPath) : Promise.resolve([]),
    runFrontend && hasFrontend ? scanFrontendPages(projectPath) : Promise.resolve([]),
    runFrontend && hasFrontend ? scanExtensionRegistries(projectPath) : Promise.resolve([]),
    runFrontend && hasFrontend ? scanI18n(projectPath) : Promise.resolve({ locales: [], namespaces: [] }),
  ]);

  const domainsRun: InventoryDomain[] = [];
  if (runDomain) domainsRun.push('domain');
  if (runData) domainsRun.push('data');
  if (runApi) domainsRun.push('api');
  if (runFrontend) domainsRun.push('frontend');

  const report: InventoryReport = {
    projectPath,
    hasDotnet,
    hasFrontend,
    domain: { entities },
    data: { migrations, seedProviders },
    api: { controllers },
    frontend: { pages, registries, i18n },
  };

  return {
    report,
    durationMs: Date.now() - start,
    domainsRun,
  };
}
