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

const execFileAsync = promisify(execFile);

const PHPCS_CONFIG_FILES = ['phpcs.xml', 'phpcs.xml.dist', '.phpcs.xml', '.phpcs.xml.dist'];
const PHPSTAN_CONFIG_FILES = ['phpstan.neon', 'phpstan.neon.dist'];

interface ToolDetection {
  configuredBinPath: string | null;
  resolvedBinPath: string | null;
  source: 'configured' | 'project-local' | 'path' | null;
  exists: boolean;
  configFile: string | null;
}

export interface PhpToolingInfo {
  supportedModes: string[];
  activeMode: string;
  phpcs: ToolDetection & {
    standard: string | null;
  };
  phpstan: ToolDetection & {
    level: string | null;
  };
}

function resolveConfiguredPath(projectPath: string | undefined, configuredPath: string | null): string | null {
  if (!configuredPath) {
    return null;
  }

  if (path.isAbsolute(configuredPath)) {
    return path.resolve(configuredPath);
  }

  if (!projectPath) {
    return null;
  }

  return path.resolve(projectPath, configuredPath);
}

function findProjectFile(projectPath: string | undefined, candidates: string[]): string | null {
  if (!projectPath) {
    return null;
  }

  for (const candidate of candidates) {
    const resolved = path.resolve(projectPath, candidate);
    if (fs.existsSync(resolved)) {
      return resolved;
    }
  }

  return null;
}

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

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

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

async function detectTool(
  projectPath: string | undefined,
  configuredBinPath: string | null,
  localCandidates: string[],
  pathCommand: string,
  configCandidates: string[]
): Promise<ToolDetection> {
  const configuredResolved = resolveConfiguredPath(projectPath, configuredBinPath);
  if (configuredResolved) {
    return {
      configuredBinPath,
      resolvedBinPath: configuredResolved,
      source: 'configured',
      exists: fs.existsSync(configuredResolved),
      configFile: findProjectFile(projectPath, configCandidates)
    };
  }

  const localResolved = findProjectFile(projectPath, localCandidates);
  if (localResolved) {
    return {
      configuredBinPath,
      resolvedBinPath: localResolved,
      source: 'project-local',
      exists: true,
      configFile: findProjectFile(projectPath, configCandidates)
    };
  }

  const pathResolved = await resolveCommandPath(pathCommand);
  if (pathResolved) {
    return {
      configuredBinPath,
      resolvedBinPath: pathResolved,
      source: 'path',
      exists: true,
      configFile: findProjectFile(projectPath, configCandidates)
    };
  }

  return {
    configuredBinPath,
    resolvedBinPath: null,
    source: null,
    exists: false,
    configFile: findProjectFile(projectPath, configCandidates)
  };
}

export async function detectPhpTooling(projectPath: string | undefined, config: PhpConfig): Promise<PhpToolingInfo> {
  const phpcs = await detectTool(
    projectPath,
    config.phpcsBinPath,
    ['vendor/bin/phpcs', 'vendor/bin/phpcs.bat'],
    'phpcs',
    PHPCS_CONFIG_FILES
  );

  const phpstan = await detectTool(
    projectPath,
    config.phpstanBinPath,
    ['vendor/bin/phpstan', 'vendor/bin/phpstan.bat'],
    'phpstan',
    PHPSTAN_CONFIG_FILES
  );

  return {
    supportedModes: ['syntax', 'phpcs', 'phpstan', 'all'],
    activeMode: config.linter,
    phpcs: {
      ...phpcs,
      standard: config.phpcsStandard
    },
    phpstan: {
      ...phpstan,
      level: config.phpstanLevel
    }
  };
}