import fs from 'fs';
import path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import dotenv from 'dotenv';
import { getConfig } from '../config.js';

const execFileAsync = promisify(execFile);

export interface PhpCandidate {
  path: string;
  version?: string;
  source: 'tool-arg' | 'project-env' | 'launcher-env' | 'path' | 'known-location';
  matchesPreference: boolean;
}

export interface PhpRuntimeResolution {
  phpPath: string;
  version?: string;
  source: 'tool-arg' | 'project-env' | 'launcher-env' | 'path' | 'known-location';
  candidates: PhpCandidate[];
  warnings: string[];
  userAction?: string;
  suggestedEnv?: Record<string, string>;
}

export interface PhpRuntimeOptions {
  phpBin?: string | null;
  phpVersion?: string | null;
}

export class PhpRuntimeError extends Error {
  runtimeError: {
    userAction: string;
    suggestedEnv: Record<string, string>;
    candidates: PhpCandidate[];
    warnings: string[];
  };

  constructor(
    message: string,
    details: {
      userAction: string;
      suggestedEnv: Record<string, string>;
      candidates?: PhpCandidate[];
      warnings?: string[];
    }
  ) {
    super(message);
    this.name = 'PhpRuntimeError';
    this.runtimeError = {
      userAction: details.userAction,
      suggestedEnv: details.suggestedEnv,
      candidates: details.candidates || [],
      warnings: details.warnings || []
    };
  }
}

interface InternalCandidate {
  path: string;
  version?: string;
  source: 'tool-arg' | 'project-env' | 'launcher-env' | 'path' | 'known-location';
}

interface InvalidStrictCandidate {
  path: string;
  source: InternalCandidate['source'];
}

function normalizeVersionPreference(preference: string | null | undefined): string | null {
  if (!preference) {
    return null;
  }

  const cleaned = preference.trim();
  return cleaned.length > 0 ? cleaned : null;
}

function parsePhpVersion(output: string): string | undefined {
  const match = output.match(/PHP\s+([0-9]+\.[0-9]+\.[0-9]+)/i);
  return match?.[1];
}

function parseVersionParts(version: string | undefined): [number, number, number] {
  if (!version) {
    return [0, 0, 0];
  }

  const [major, minor, patch] = version.split('.').map((part) => Number.parseInt(part, 10) || 0);
  return [major, minor, patch];
}

function compareVersions(a: string | undefined, b: string | undefined): number {
  const aParts = parseVersionParts(a);
  const bParts = parseVersionParts(b);

  if (aParts[0] !== bParts[0]) return aParts[0] - bParts[0];
  if (aParts[1] !== bParts[1]) return aParts[1] - bParts[1];
  return aParts[2] - bParts[2];
}

function versionMatchesPreference(version: string | undefined, preference: string | null): boolean {
  if (!version || !preference) {
    return false;
  }

  // Accept major (e.g. 8), major.minor (e.g. 8.3) or full version.
  const versionParts = version.split('.');
  const preferenceParts = preference.split('.');

  if (preferenceParts.length === 1) {
    return versionParts[0] === preferenceParts[0];
  }

  if (preferenceParts.length === 2) {
    return versionParts[0] === preferenceParts[0] && versionParts[1] === preferenceParts[1];
  }

  return version === preference;
}

function normalizeOptional(value: string | null | undefined): string | null {
  if (!value) {
    return null;
  }

  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : null;
}

function suggestedEnv(phpBin?: string | null, phpVersion?: string | null): Record<string, string> {
  const env: Record<string, string> = {
    PHP_LINTER: 'syntax'
  };

  if (phpBin) {
    env.PHP_BIN = phpBin;
  }

  if (phpVersion) {
    env.PHP_VERSION_PREFERENCE = phpVersion;
  }

  return env;
}

function suggestedEnvFromCandidates(
  candidates: InternalCandidate[],
  preference: string | null,
  fallbackPhpBin?: string | null,
  fallbackPhpVersion?: string | null
): Record<string, string> {
  if (candidates.length === 0) {
    return suggestedEnv(fallbackPhpBin, fallbackPhpVersion);
  }

  const selected = pickBestCandidate(candidates, preference);
  return suggestedEnv(
    selected.path,
    selected.version ? selected.version.split('.').slice(0, 2).join('.') : (fallbackPhpVersion || preference)
  );
}

function tryReadProjectEnv(projectPath?: string): Record<string, string> {
  if (!projectPath) {
    return {};
  }

  const envPath = path.resolve(projectPath, '.env');
  if (!fs.existsSync(envPath)) {
    return {};
  }

  try {
    const content = fs.readFileSync(envPath, 'utf8');
    return dotenv.parse(content);
  } catch {
    return {};
  }
}

async function resolveCommandPath(command: string): Promise<string | null> {
  const shellCmd = process.platform === 'win32' ? 'where' : 'which';

  try {
    const { stdout } = await execFileAsync(shellCmd, [command], { windowsHide: true });
    const first = stdout
      .split(/\r?\n/)
      .map((line) => line.trim())
      .find((line) => line.length > 0);

    if (!first) {
      return null;
    }

    return path.resolve(first);
  } catch {
    return null;
  }
}

async function validatePhpCandidate(candidatePath: string, source: InternalCandidate['source']): Promise<InternalCandidate | null> {
  try {
    const { stdout, stderr } = await execFileAsync(candidatePath, ['-v'], { windowsHide: true, timeout: 8000 });
    const version = parsePhpVersion(`${stdout || ''}\n${stderr || ''}`);
    return { path: candidatePath, source, version };
  } catch {
    return null;
  }
}

async function discoverWindowsKnownLocations(): Promise<string[]> {
  const candidates: string[] = [];

  const pushIfExists = (p: string) => {
    if (fs.existsSync(p)) {
      candidates.push(path.resolve(p));
    }
  };

  const laragonRoot = 'C:\\laragon\\bin\\php';
  if (fs.existsSync(laragonRoot)) {
    for (const entry of fs.readdirSync(laragonRoot, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      pushIfExists(path.join(laragonRoot, entry.name, 'php.exe'));
    }
  }

  pushIfExists('C:\\xampp\\php\\php.exe');
  const windowsRoot = 'C:\\';
  if (fs.existsSync(windowsRoot)) {
    for (const entry of fs.readdirSync(windowsRoot, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      if (!entry.name.toLowerCase().startsWith('xampp')) continue;
      pushIfExists(path.join(windowsRoot, entry.name, 'php', 'php.exe'));
    }
  }

  for (const wampRoot of ['C:\\wamp64\\bin\\php', 'C:\\wamp\\bin\\php']) {
    if (!fs.existsSync(wampRoot)) continue;
    for (const entry of fs.readdirSync(wampRoot, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      pushIfExists(path.join(wampRoot, entry.name, 'php.exe'));
    }
  }

  const toolsRoot = 'C:\\tools';
  if (fs.existsSync(toolsRoot)) {
    for (const entry of fs.readdirSync(toolsRoot, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      if (!entry.name.toLowerCase().startsWith('php')) continue;
      pushIfExists(path.join(toolsRoot, entry.name, 'php.exe'));
    }
  }

  pushIfExists('C:\\Program Files\\PHP\\php.exe');
  const programFiles = 'C:\\Program Files';
  if (fs.existsSync(programFiles)) {
    for (const entry of fs.readdirSync(programFiles, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      if (!entry.name.toLowerCase().startsWith('php')) continue;
      pushIfExists(path.join(programFiles, entry.name, 'php.exe'));
    }
  }

  pushIfExists('C:\\ProgramData\\chocolatey\\bin\\php.exe');

  if (process.env.USERPROFILE) {
    pushIfExists(path.join(process.env.USERPROFILE, 'scoop', 'apps', 'php', 'current', 'php.exe'));
  }

  return Array.from(new Set(candidates));
}

async function discoverUnixKnownLocations(): Promise<string[]> {
  const candidates: string[] = [];

  const pushIfExists = (p: string) => {
    if (fs.existsSync(p)) {
      candidates.push(path.resolve(p));
    }
  };

  pushIfExists('/usr/bin/php');
  pushIfExists('/usr/local/bin/php');
  pushIfExists('/snap/bin/php');

  const optPhpRoot = '/opt/php';
  if (fs.existsSync(optPhpRoot)) {
    for (const entry of fs.readdirSync(optPhpRoot, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      pushIfExists(path.join(optPhpRoot, entry.name, 'bin', 'php'));
    }
  }

  try {
    const { stdout } = await execFileAsync('update-alternatives', ['--list', 'php'], { windowsHide: true, timeout: 8000 });
    for (const line of stdout.split(/\r?\n/)) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      if (fs.existsSync(trimmed)) {
        candidates.push(path.resolve(trimmed));
      }
    }
  } catch {
    // Optional command, safe to ignore.
  }

  return Array.from(new Set(candidates));
}

function pickBestCandidate(candidates: InternalCandidate[], preference: string | null): InternalCandidate {
  const sorted = [...candidates].sort((a, b) => {
    const prefA = versionMatchesPreference(a.version, preference) ? 1 : 0;
    const prefB = versionMatchesPreference(b.version, preference) ? 1 : 0;
    if (prefA !== prefB) return prefB - prefA;

    const versionCmp = compareVersions(a.version, b.version);
    if (versionCmp !== 0) return -versionCmp;

    const sourceRank: Record<InternalCandidate['source'], number> = {
      'tool-arg': 0,
      'project-env': 1,
      'launcher-env': 2,
      path: 3,
      'known-location': 4
    };

    return sourceRank[a.source] - sourceRank[b.source];
  });

  return sorted[0];
}

function toPublicCandidates(candidates: InternalCandidate[], preference: string | null): PhpCandidate[] {
  return candidates.map((candidate) => ({
    path: candidate.path,
    version: candidate.version,
    source: candidate.source,
    matchesPreference: versionMatchesPreference(candidate.version, preference)
  }));
}

export async function resolvePhpRuntime(projectPath?: string, options: PhpRuntimeOptions = {}): Promise<PhpRuntimeResolution> {
  const currentConfig = getConfig(projectPath);
  const projectEnv = tryReadProjectEnv(projectPath);

  const toolPhpBin = normalizeOptional(options.phpBin);
  const toolPhpVersion = normalizeOptional(options.phpVersion);
  const projectEnvPhpBin = projectEnv.PHP_BIN && projectEnv.PHP_BIN.trim().length > 0
    ? projectEnv.PHP_BIN.trim()
    : null;
  const launcherPhpBin = !projectEnvPhpBin && currentConfig.php.binPath
    ? currentConfig.php.binPath
    : null;

  const versionPreference = normalizeVersionPreference(toolPhpVersion || currentConfig.php.versionPreference);
  const strictVersionPreference = toolPhpVersion !== null;

  const warnings: string[] = [];
  const byPath = new Map<string, InternalCandidate>();
  let invalidStrictCandidate: InvalidStrictCandidate | null = null;

  const buildCandidates = () => toPublicCandidates(Array.from(byPath.values()), versionPreference);

  const addCandidate = async (candidatePath: string, source: InternalCandidate['source'], strict: boolean = false) => {
    const normalizedPath = path.resolve(candidatePath);
    if (byPath.has(normalizedPath)) {
      return;
    }

    const candidate = await validatePhpCandidate(normalizedPath, source);
    if (!candidate) {
      if (strict) {
        invalidStrictCandidate = {
          path: candidatePath,
          source
        };
        warnings.push(`Runtime PHP configurato ma non valido (${source}): ${candidatePath}`);
        return;
      }
      warnings.push(`Runtime PHP non valido (${source}): ${candidatePath}`);
      return;
    }

    byPath.set(normalizedPath, candidate);
  };

  if (toolPhpBin) {
    await addCandidate(toolPhpBin, 'tool-arg', true);
  }

  if (!toolPhpBin && projectEnvPhpBin) {
    await addCandidate(projectEnvPhpBin, 'project-env', true);
  }

  if (launcherPhpBin && launcherPhpBin !== toolPhpBin && launcherPhpBin !== projectEnvPhpBin) {
    await addCandidate(launcherPhpBin, 'launcher-env');
  }

  const phpFromPath = await resolveCommandPath('php');
  if (phpFromPath) {
    await addCandidate(phpFromPath, 'path');
  }

  const knownLocations = process.platform === 'win32'
    ? await discoverWindowsKnownLocations()
    : await discoverUnixKnownLocations();

  for (const candidatePath of knownLocations) {
    await addCandidate(candidatePath, 'known-location');
  }

  const candidates = Array.from(byPath.values());

  if (invalidStrictCandidate) {
    const strictInvalidPath = (invalidStrictCandidate as InvalidStrictCandidate).path;
    throw new PhpRuntimeError(
      `Runtime PHP configurato ma non valido: ${strictInvalidPath}`,
      {
        userAction: `Verifica che il file esista e che '${strictInvalidPath} -v' funzioni. Correggi PHP_BIN nel .env del project_path oppure passa php_bin valido nella chiamata tool.`,
        suggestedEnv: suggestedEnvFromCandidates(candidates, versionPreference, undefined, versionPreference),
        candidates: buildCandidates(),
        warnings
      }
    );
  }

  if (candidates.length === 0) {
    const osHint = process.platform === 'win32'
      ? 'Installa PHP CLI oppure imposta PHP_BIN (es. Laragon/XAMPP).'
      : 'Installa PHP CLI (es. sudo apt install php-cli) oppure imposta PHP_BIN.';
    throw new PhpRuntimeError(
      `PHP CLI non trovato. ${osHint}`,
      {
        userAction: `${osHint} Per un progetto legacy puoi creare un .env nel project_path con PHP_BIN e PHP_VERSION_PREFERENCE.`,
        suggestedEnv: suggestedEnv(undefined, versionPreference),
        warnings
      }
    );
  }

  const selected = pickBestCandidate(candidates, versionPreference);
  const publicCandidates = toPublicCandidates(candidates, versionPreference);
  const explicitToolCandidate = toolPhpBin
    ? publicCandidates.find((candidate) => path.resolve(candidate.path) === path.resolve(toolPhpBin))
    : undefined;

  if (strictVersionPreference && explicitToolCandidate && !explicitToolCandidate.matchesPreference) {
    throw new PhpRuntimeError(
      `Il runtime PHP richiesto da php_bin non combacia con php_version=${toolPhpVersion}.`,
      {
        userAction: `Il binario '${toolPhpBin}' risponde con PHP ${explicitToolCandidate.version || 'versione non rilevata'}, ma il tool ha richiesto PHP ${toolPhpVersion}. Passa un php_bin coerente oppure correggi php_version.`,
        suggestedEnv: suggestedEnv(toolPhpBin, toolPhpVersion),
        candidates: publicCandidates,
        warnings
      }
    );
  }

  if (strictVersionPreference && !publicCandidates.some((candidate) => candidate.matchesPreference)) {
    const formatted = publicCandidates
      .map((candidate) => `${candidate.path}${candidate.version ? ` (${candidate.version})` : ''}`)
      .join(', ');
    throw new PhpRuntimeError(
      `Nessun runtime PHP combacia con php_version=${toolPhpVersion}.`,
      {
        userAction: `Installa o configura un PHP ${toolPhpVersion} valido. In alternativa passa php_bin al tool oppure imposta PHP_BIN e PHP_VERSION_PREFERENCE nel .env del project_path. Candidati trovati: ${formatted || 'nessuno'}.`,
        suggestedEnv: suggestedEnv(undefined, toolPhpVersion),
        candidates: publicCandidates,
        warnings
      }
    );
  }

  if (candidates.length > 1) {
    const formatted = candidates
      .map((c) => `${c.path}${c.version ? ` (${c.version})` : ''}`)
      .join(', ');
    warnings.push(`Rilevati piu runtime PHP: ${formatted}`);
  }

  if (versionPreference && !versionMatchesPreference(selected.version, versionPreference)) {
    warnings.push(`Nessun runtime PHP combacia con PHP_VERSION_PREFERENCE=${versionPreference}. Selezionato ${selected.path}.`);
  }

  return {
    phpPath: selected.path,
    version: selected.version,
    source: selected.source,
    candidates: publicCandidates,
    warnings,
    userAction: warnings.length > 0
      ? 'Verifica che il runtime PHP selezionato sia quello atteso per il progetto. Per forzarlo, imposta PHP_BIN/PHP_VERSION_PREFERENCE nel .env oppure passa php_bin/php_version al tool.'
      : undefined,
    suggestedEnv: suggestedEnv(selected.path, selected.version ? selected.version.split('.').slice(0, 2).join('.') : versionPreference)
  };
}
