import fs from 'node:fs';
import path from 'node:path';
import type { AdapterCommand, FrameworkAdapter } from './types.js';
import {
  detectJsPackageManager,
  hasDependency,
  installCommandFor,
  readPackageJson,
  runScriptCommandFor,
  type JsPackageManager,
  type PackageJson,
} from './packageManager.js';

type Bundler = 'vite' | 'next' | 'create-react-app' | 'unknown';
export type NextRouter = 'app' | 'pages' | 'unknown';

function detectBundler(root: string, pkg: PackageJson | null): Bundler {
  if (hasDependency(pkg, 'next') || fileExists(root, ['next.config.js', 'next.config.mjs', 'next.config.ts'])) {
    return 'next';
  }
  if (fileExists(root, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'])) return 'vite';
  if (hasDependency(pkg, 'react-scripts')) return 'create-react-app';
  return 'unknown';
}

function detectNextRouter(root: string): NextRouter {
  const appLayouts = [
    path.join(root, 'app', 'layout.tsx'),
    path.join(root, 'app', 'layout.jsx'),
    path.join(root, 'src', 'app', 'layout.tsx'),
    path.join(root, 'src', 'app', 'layout.jsx'),
  ];
  if (appLayouts.some((p) => fs.existsSync(p))) return 'app';

  const pagesIndexes = [
    path.join(root, 'pages', 'index.tsx'),
    path.join(root, 'pages', 'index.jsx'),
    path.join(root, 'pages', 'index.js'),
    path.join(root, 'src', 'pages', 'index.tsx'),
    path.join(root, 'src', 'pages', 'index.jsx'),
  ];
  if (pagesIndexes.some((p) => fs.existsSync(p))) return 'pages';
  return 'unknown';
}

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

export class ReactAdapter implements FrameworkAdapter {
  readonly kind = 'react' as const;
  readonly label: string;
  readonly packageManager: JsPackageManager;
  private readonly pkg: PackageJson | null;
  private readonly bundler: Bundler;
  private readonly nextRouter: NextRouter;

  constructor(readonly root: string) {
    this.pkg = readPackageJson(root);
    this.packageManager = detectJsPackageManager(root);
    this.bundler = detectBundler(root, this.pkg);
    this.nextRouter = this.bundler === 'next' ? detectNextRouter(root) : 'unknown';
    this.label = `React (${this.bundler})`;
  }

  installCommand(): AdapterCommand {
    const { command, args } = installCommandFor(this.packageManager);
    return { command, args, cwd: this.root };
  }

  devCommand(): AdapterCommand {
    const script = this.findScript(['dev', 'start']);
    if (!script) {
      throw new Error(`No "dev" or "start" script found in package.json at "${this.root}".`);
    }
    const { command, args } = runScriptCommandFor(this.packageManager, script);
    return { command, args, cwd: this.root };
  }

  buildCommand(): AdapterCommand | null {
    return this.scriptCommandOrNull(['build']);
  }

  testCommand(extraArgs: string[] = []): AdapterCommand | null {
    return this.scriptCommandOrNull(['test'], extraArgs);
  }

  lintCommand(fix = false): AdapterCommand | null {
    return this.scriptCommandOrNull(fix ? ['format', 'lint:fix'] : ['lint']);
  }

  readyPattern(): RegExp {
    switch (this.bundler) {
      case 'vite':
        return /Local:\s+(https?:\/\/\S+)/i;
      case 'next':
        return /(?:Local|ready)[^:]*:\s*(https?:\/\/\S+)|started server on[^,]*,\s*url:\s*(https?:\/\/\S+)/i;
      case 'create-react-app':
        return /Local:\s+(https?:\/\/\S+)|Compiled successfully/i;
      default:
        return /(https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)[:\d]*\S*)/i;
    }
  }

  describe(): Record<string, unknown> {
    const isNext = this.bundler === 'next';
    return {
      kind: this.kind,
      label: this.label,
      root: this.root,
      packageManager: this.packageManager,
      bundler: this.bundler,
      router: isNext ? this.nextRouter : null,
      rsc: isNext && this.nextRouter === 'app',
      reactVersion: this.pkg?.dependencies?.react ?? this.pkg?.devDependencies?.react ?? null,
      typescript: hasDependency(this.pkg, 'typescript') || fileExists(this.root, ['tsconfig.json']),
      scripts: this.pkg?.scripts ?? {},
    };
  }

  private findScript(candidates: string[]): string | undefined {
    return candidates.find((name) => this.pkg?.scripts?.[name]);
  }

  private scriptCommandOrNull(candidates: string[], extraArgs: string[] = []): AdapterCommand | null {
    const script = this.findScript(candidates);
    if (!script) return null;
    const { command, args } = runScriptCommandFor(this.packageManager, script, extraArgs);
    return { command, args, cwd: this.root };
  }
}
