import fs from 'node:fs';
import path from 'node:path';
import { readComposerJson, hasComposerDependency } from './composer.js';
import { hasDependency, readPackageJson } from './packageManager.js';
import { LaravelAdapter } from './laravel.js';
import { CodeIgniterAdapter } from './codeigniter.js';
import { ReactAdapter } from './react.js';
import { VueAdapter, NuxtAdapter } from './vue.js';
import { DjangoAdapter, RailsAdapter, SymfonyAdapter } from './djangoRailsSymfony.js';
import { GenericAdapter } from './generic.js';
import type { DetectedPackage, DetectedProject, DetectedWorkspace, FrameworkAdapter } from './types.js';

const FRONTEND_SUBDIRS = ['frontend', 'client', 'web', 'resources/js-app'];
const MONOREPO_GLOBS = ['apps', 'packages', 'services', 'libs'];

function fileExists(root: string, names: string[]): boolean {
  return names.some((name) => fs.existsSync(path.join(root, name)));
}

function hasVueSfc(root: string): boolean {
  const src = path.join(root, 'src');
  if (!fs.existsSync(src)) return false;
  try {
    const stack = [src];
    let checked = 0;
    while (stack.length > 0 && checked < 200) {
      const dir = stack.pop()!;
      for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        checked += 1;
        if (entry.isDirectory() && entry.name !== 'node_modules') {
          stack.push(path.join(dir, entry.name));
        } else if (entry.isFile() && entry.name.endsWith('.vue')) {
          return true;
        }
      }
    }
  } catch {
    return false;
  }
  return false;
}

function detectPhpOrSymfonyBackend(root: string): FrameworkAdapter | null {
  const composerJson = readComposerJson(root);
  if (!composerJson) return null;

  const hasArtisan = fs.existsSync(path.join(root, 'artisan'));
  const hasSpark = fs.existsSync(path.join(root, 'spark'));

  if (hasComposerDependency(composerJson, 'laravel/framework') || hasArtisan) {
    return new LaravelAdapter(root);
  }
  if (hasComposerDependency(composerJson, 'codeigniter4/framework') || hasSpark) {
    return new CodeIgniterAdapter(root);
  }
  if (
    hasComposerDependency(composerJson, 'symfony/framework-bundle') ||
    hasComposerDependency(composerJson, 'symfony/symfony') ||
    fileExists(root, ['bin/console', 'symfony.lock'])
  ) {
    return new SymfonyAdapter(root);
  }
  return null;
}

function detectDjangoAt(root: string): FrameworkAdapter | null {
  if (fileExists(root, ['manage.py'])) return new DjangoAdapter(root);
  for (const name of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml']) {
    const file = path.join(root, name);
    if (!fs.existsSync(file)) continue;
    const text = fs.readFileSync(file, 'utf8').toLowerCase();
    if (/\bdjango\b/.test(text)) return new DjangoAdapter(root);
  }
  return null;
}

function detectRailsAt(root: string): FrameworkAdapter | null {
  const gemfile = path.join(root, 'Gemfile');
  if (fs.existsSync(gemfile)) {
    const text = fs.readFileSync(gemfile, 'utf8').toLowerCase();
    if (/\brails\b/.test(text) || fileExists(root, ['bin/rails', 'config/application.rb'])) {
      return new RailsAdapter(root);
    }
  }
  if (fileExists(root, ['bin/rails', 'config/application.rb'])) {
    return new RailsAdapter(root);
  }
  return null;
}

function detectBackend(root: string): FrameworkAdapter | null {
  return detectPhpOrSymfonyBackend(root) ?? detectDjangoAt(root) ?? detectRailsAt(root);
}

function detectFrontendAt(root: string): FrameworkAdapter | null {
  const pkg = readPackageJson(root);
  if (
    hasDependency(pkg, 'nuxt') ||
    fileExists(root, ['nuxt.config.js', 'nuxt.config.ts', 'nuxt.config.mjs'])
  ) {
    return new NuxtAdapter(root);
  }
  if (
    hasDependency(pkg, 'vue') ||
    (hasVueSfc(root) && fileExists(root, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']))
  ) {
    return new VueAdapter(root);
  }
  if (hasDependency(pkg, 'react')) return new ReactAdapter(root);
  return null;
}

/**
 * Inspects `workspaceRoot` and returns the detected backend/frontend
 * adapter(s). Supports PHP (Laravel/CI/Symfony), Django, Rails backends and
 * React/Vue/Nuxt frontends, including sibling frontend folders.
 */
export function detectProject(workspaceRoot: string): DetectedProject {
  const backend = detectBackend(workspaceRoot);
  let frontend = detectFrontendAt(workspaceRoot);

  if (backend && !frontend) {
    for (const subdir of FRONTEND_SUBDIRS) {
      const candidateRoot = path.join(workspaceRoot, subdir);
      if (!fs.existsSync(candidateRoot)) continue;
      const candidate = detectFrontendAt(candidateRoot);
      if (candidate) {
        frontend = candidate;
        break;
      }
    }
  }

  // Pure frontend-only Vue/Nuxt/React at root without backend.
  if (!backend && !frontend) {
    frontend = detectFrontendAt(workspaceRoot);
  }

  if (backend) {
    return { backend, frontend, primary: backend };
  }
  if (frontend) {
    return { backend: null, frontend, primary: frontend };
  }

  const generic = new GenericAdapter(workspaceRoot);
  return { backend: null, frontend: null, primary: generic };
}

export function summarizeDetection(detected: DetectedProject): Record<string, unknown> {
  return {
    backend: detected.backend?.describe() ?? null,
    frontend: detected.frontend?.describe() ?? null,
    primary: detected.primary.kind,
    combined: Boolean(detected.backend && detected.frontend),
  };
}

function readWorkspaceGlobs(root: string): string[] {
  const globs = new Set<string>([...MONOREPO_GLOBS, ...FRONTEND_SUBDIRS.map((d) => d.split('/')[0]!).filter(Boolean)]);

  const pkg = readPackageJson(root);
  const workspaces = pkg?.workspaces;
  if (Array.isArray(workspaces)) {
    for (const entry of workspaces) {
      if (typeof entry === 'string' && entry.includes('/*')) {
        globs.add(entry.replace(/\/\*$/, ''));
      }
    }
  } else if (workspaces && typeof workspaces === 'object' && Array.isArray((workspaces as { packages?: string[] }).packages)) {
    for (const entry of (workspaces as { packages: string[] }).packages) {
      if (entry.includes('/*')) globs.add(entry.replace(/\/\*$/, ''));
    }
  }

  const pnpm = path.join(root, 'pnpm-workspace.yaml');
  if (fs.existsSync(pnpm)) {
    const text = fs.readFileSync(pnpm, 'utf8');
    for (const match of text.matchAll(/['"]?([A-Za-z0-9_-]+)\/\*['"]?/g)) {
      if (match[1]) globs.add(match[1]);
    }
  }

  return [...globs];
}

function listImmediateSubdirs(dir: string): string[] {
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
  return fs
    .readdirSync(dir, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'vendor')
    .map((entry) => entry.name);
}

function packageLooksLikeProject(dir: string): boolean {
  return (
    fs.existsSync(path.join(dir, 'package.json')) ||
    fs.existsSync(path.join(dir, 'composer.json')) ||
    fs.existsSync(path.join(dir, 'artisan')) ||
    fs.existsSync(path.join(dir, 'spark')) ||
    fs.existsSync(path.join(dir, 'manage.py')) ||
    fs.existsSync(path.join(dir, 'Gemfile')) ||
    fs.existsSync(path.join(dir, 'bin', 'console'))
  );
}

/**
 * Scan the workspace for monorepo packages under apps/*, packages/*, services/*,
 * frontend/client/web, plus the workspace root itself.
 */
export function detectWorkspace(workspaceRoot: string): DetectedWorkspace {
  const absoluteRoot = path.resolve(workspaceRoot);
  const packages: DetectedPackage[] = [];
  const seen = new Set<string>();

  const addPackage = (name: string, absolutePath: string) => {
    const normalized = path.resolve(absolutePath);
    if (seen.has(normalized.toLowerCase())) return;
    if (!packageLooksLikeProject(normalized) && normalized !== absoluteRoot) return;
    seen.add(normalized.toLowerCase());
    const relativePath = path.relative(absoluteRoot, normalized) || '.';
    packages.push({
      name,
      relativePath: relativePath === '' ? '.' : relativePath.replace(/\\/g, '/'),
      absolutePath: normalized,
      detected: detectProject(normalized),
    });
  };

  addPackage(path.basename(absoluteRoot) || 'root', absoluteRoot);

  for (const globRoot of readWorkspaceGlobs(absoluteRoot)) {
    const base = path.join(absoluteRoot, globRoot);
    for (const child of listImmediateSubdirs(base)) {
      addPackage(child, path.join(base, child));
    }
    if (FRONTEND_SUBDIRS.some((d) => d === globRoot || d.startsWith(`${globRoot}/`)) && packageLooksLikeProject(base)) {
      addPackage(globRoot, base);
    }
  }

  return { root: absoluteRoot, packages };
}

export function summarizeWorkspace(workspace: DetectedWorkspace): Record<string, unknown> {
  return {
    root: workspace.root,
    packageCount: workspace.packages.length,
    packages: workspace.packages.map((pkg) => ({
      name: pkg.name,
      path: pkg.relativePath,
      primary: pkg.detected.primary.kind,
      combined: Boolean(pkg.detected.backend && pkg.detected.frontend),
      backend: pkg.detected.backend?.kind ?? null,
      frontend: pkg.detected.frontend?.kind ?? null,
    })),
  };
}

/**
 * Resolve a package name or relative path to a DetectedPackage inside the workspace.
 */
export function resolvePackage(
  workspaceRoot: string,
  packageNameOrPath: string | undefined,
): { workspace: DetectedWorkspace; pkg: DetectedPackage } {
  const workspace = detectWorkspace(workspaceRoot);
  if (!packageNameOrPath) {
    const rootPkg = workspace.packages.find((p) => p.relativePath === '.') ?? workspace.packages[0];
    if (!rootPkg) throw new Error('No packages detected in the workspace.');
    return { workspace, pkg: rootPkg };
  }

  const needle = packageNameOrPath.replace(/\\/g, '/').replace(/^\.\//, '');
  const match = workspace.packages.find(
    (p) =>
      p.name === packageNameOrPath ||
      p.relativePath === needle ||
      p.relativePath === packageNameOrPath ||
      p.absolutePath.replace(/\\/g, '/').endsWith(`/${needle}`),
  );
  if (!match) {
    const available = workspace.packages.map((p) => `${p.name} (${p.relativePath})`).join(', ');
    throw new Error(`Unknown package "${packageNameOrPath}". Available: ${available || '(none)'}.`);
  }
  return { workspace, pkg: match };
}
