/**
 * detect-dbcontexts.ts — Discover EF Core DbContexts in a worktree.
 *
 * Strategy: find every *ModelSnapshot.cs, read its `partial class Xxx`
 * declaration, then associate it with the .csproj that owns the snapshot's
 * migrations folder. Multiple snapshots for the same context (across
 * Migrations.Sqlite / Postgres / SqlServer assemblies) are grouped as one
 * context with multiple "assemblies".
 *
 * Return shape is consumed by list/status/create/squash/rebase-snapshot CLIs
 * and by the Studio's frontend MigrationsTab to render a context picker.
 */
import path from 'node:path';
import { findFiles, readText, fileExists } from '../../../lib/fs.js';
import { parseCsprojVersion } from './parse-csproj-version.js';

export interface MigrationAssembly {
  /** Absolute path to the .csproj that owns the migrations folder. */
  csprojPath: string;
  /** Assembly name derived from csproj filename (minus ".csproj"). */
  assemblyName: string;
  /** Absolute path to the directory containing the migrations + snapshot. */
  migrationsDir: string;
  /** Snapshot file inside migrationsDir. */
  snapshotPath: string;
  /** Inferred provider: "sqlite" / "postgres" / "sqlserver" / null. */
  provider: string | null;
  /** Version read from the csproj <Version> element (or "0.1.0" fallback). */
  version: string;
}

export interface DbContextInfo {
  /** Partial-class name from the snapshot (e.g. "CoreDbContext"). */
  name: string;
  /** One entry per migration assembly that targets this context. */
  assemblies: MigrationAssembly[];
}

export interface DetectionResult {
  /** Kind heuristic: "generated" app (Core+Extensions) / "studio" / "unknown". */
  kind: 'generated' | 'studio' | 'unknown';
  contexts: DbContextInfo[];
  /** Absolute working directory used for detection. */
  cwd: string;
}

const PROVIDER_HINTS: Array<[RegExp, string]> = [
  [/\bSqlite\b/i, 'sqlite'],
  [/\bPostgres(?:ql)?\b/i, 'postgres'],
  [/\bNpgsql\b/i, 'postgres'],
  [/\bSqlServer\b/i, 'sqlserver'],
  [/\bMssql\b/i, 'sqlserver'],
];

function inferProvider(csprojPath: string, migrationsDir: string): string | null {
  const haystack = `${csprojPath} ${migrationsDir}`;
  for (const [re, name] of PROVIDER_HINTS) {
    if (re.test(haystack)) return name;
  }
  return null;
}

async function extractContextNameFromSnapshot(snapshotPath: string): Promise<string | null> {
  try {
    const text = await readText(snapshotPath);
    // Matches both:
    //   [DbContext(typeof(CoreDbContext))]
    //   partial class CoreDbContextModelSnapshot : ModelSnapshot
    const attr = text.match(/DbContext\s*\(\s*typeof\s*\(\s*(\w+)\s*\)/);
    if (attr) return attr[1];
    const cls = text.match(/partial\s+class\s+(\w+?)ModelSnapshot\b/);
    if (cls) return cls[1];
    return null;
  } catch {
    return null;
  }
}

async function findOwningCsproj(snapshotPath: string, cwd: string): Promise<string | null> {
  // Walk up from the snapshot file looking for a .csproj in the same directory
  // tree. EF Core always places migrations inside the project that references
  // `Microsoft.EntityFrameworkCore.Design`.
  let dir = path.dirname(snapshotPath);
  const cwdResolved = path.resolve(cwd);
  while (path.resolve(dir).startsWith(cwdResolved)) {
    const csprojFiles = await findFiles('*.csproj', { cwd: dir });
    const direct = csprojFiles.filter((f) => path.dirname(f) === dir);
    if (direct.length > 0) return direct[0];
    const parent = path.dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

function classify(contexts: DbContextInfo[]): DetectionResult['kind'] {
  const names = new Set(contexts.map((c) => c.name.toLowerCase()));
  if (names.has('studiodbcontext')) return 'studio';
  // SmartStack-generated apps expose CoreDbContext, ExtensionsDbContext, or
  // both. Older / minimal scaffolds may ship only Extensions.
  if (names.has('coredbcontext') || names.has('extensionsdbcontext')) return 'generated';
  if (contexts.length > 0) return 'unknown';
  return 'unknown';
}

export async function detectDbContexts(cwd: string): Promise<DetectionResult> {
  const snapshots = await findFiles('**/Migrations/**/*ModelSnapshot.cs', { cwd });

  const grouped = new Map<string, DbContextInfo>();

  for (const snapshot of snapshots) {
    const name = await extractContextNameFromSnapshot(snapshot);
    if (!name) continue;

    const csproj = await findOwningCsproj(snapshot, cwd);
    if (!csproj) continue;

    const migrationsDir = path.dirname(snapshot);
    const assemblyName = path.basename(csproj, '.csproj');
    const version = await parseCsprojVersion(csproj);
    const provider = inferProvider(csproj, migrationsDir);

    const entry = grouped.get(name) ?? { name, assemblies: [] };
    // Skip duplicates (same assembly found twice by the glob)
    if (!entry.assemblies.some((a) => a.csprojPath === csproj && a.migrationsDir === migrationsDir)) {
      entry.assemblies.push({
        csprojPath: csproj,
        assemblyName,
        migrationsDir,
        snapshotPath: snapshot,
        provider,
        version,
      });
    }
    grouped.set(name, entry);
  }

  const contexts = Array.from(grouped.values()).map((c) => ({
    ...c,
    assemblies: c.assemblies.sort((a, b) => a.assemblyName.localeCompare(b.assemblyName)),
  }));

  return {
    kind: classify(contexts),
    contexts: contexts.sort((a, b) => a.name.localeCompare(b.name)),
    cwd: path.resolve(cwd),
  };
}

/**
 * Startup-project heuristic: EF Core design-time commands need a project that
 * references the DbContext AND can be run (Microsoft.NET.Sdk.Web or an .exe
 * Sdk). We pick a project whose name ends with `.Host` / `.Api` / `.Web`;
 * failing that, the first assembly in alphabetical order.
 */
export async function findStartupProject(cwd: string): Promise<string | null> {
  const all = await findFiles('**/*.csproj', { cwd });
  const ranked = all
    .map((f) => ({ f, base: path.basename(f, '.csproj') }))
    .sort((a, b) => {
      const score = (name: string) => {
        if (/\.Host$/i.test(name)) return 0;
        if (/\.Api$/i.test(name)) return 1;
        if (/\.Web$/i.test(name)) return 2;
        return 9;
      };
      const sa = score(a.base);
      const sb = score(b.base);
      if (sa !== sb) return sa - sb;
      return a.base.localeCompare(b.base);
    });
  for (const { f } of ranked) {
    try {
      const content = await readText(f);
      if (/<Project\s+Sdk="Microsoft\.NET\.Sdk\.Web"/i.test(content)) return f;
      if (/<OutputType>\s*Exe\s*<\/OutputType>/i.test(content)) return f;
    } catch {
      continue;
    }
  }
  return ranked[0]?.f ?? null;
}

export async function hasDotnetProject(cwd: string): Promise<boolean> {
  const anyCsproj = (await findFiles('**/*.csproj', { cwd })).length > 0;
  if (!anyCsproj) return false;
  const slnOrSlnx =
    (await findFiles('**/*.sln', { cwd })).length > 0 ||
    (await findFiles('**/*.slnx', { cwd })).length > 0;
  return slnOrSlnx || anyCsproj;
}

export async function pathExists(p: string): Promise<boolean> {
  return fileExists(p);
}
