#!/usr/bin/env php
<?php

declare(strict_types=1);

const VERSION = '1.0.0';
const HOSTS_BEGIN = '# BEGIN php-dev-router';
const HOSTS_END = '# END php-dev-router';

main($argv);

function main(array $argv): void
{
    $command = $argv[1] ?? 'help';
    $options = parseOptions(array_slice($argv, 2));
    $config = loadConfig($options['config'] ?? null);

    try {
        match ($command) {
            'scan' => commandScan($config, $options),
            'apply' => commandApply($config, $options),
            'watch' => commandWatch($config, $options),
            'doctor', 'status' => commandDoctor($config),
            'install' => commandInstall($config, $options),
            'help', '-h', '--help' => commandHelp(),
            default => fail("Unknown command: {$command}\nRun: php-dev-router help"),
        };
    } catch (RuntimeException $exception) {
        fwrite(STDERR, "php-dev-router: {$exception->getMessage()}\n");
        exit(1);
    }
}

function parseOptions(array $args): array
{
    $options = [];
    foreach ($args as $arg) {
        if (!str_starts_with($arg, '--')) {
            $options['_'][] = $arg;
            continue;
        }

        $arg = substr($arg, 2);
        if (str_contains($arg, '=')) {
            [$key, $value] = explode('=', $arg, 2);
            $options[$key] = $value;
        } else {
            $options[$arg] = true;
        }
    }

    return $options;
}

function loadConfig(?string $path): array
{
    $projectRoot = dirname(__DIR__);
    $configPath = $path ? expandPath($path) : defaultConfigPath($projectRoot);
    if (!is_file($configPath)) {
        fail("Config file not found: {$configPath}");
    }

    $config = json_decode((string) file_get_contents($configPath), true);
    if (!is_array($config)) {
        fail("Config file is not valid JSON: {$configPath}");
    }

    $defaults = [
        'projects_root' => getenv('HOME') . '/PhpstormProjects',
        'domain' => 'php.test',
        'ssl_certificate' => getenv('HOME') . '/.ssl/php-dev-router/fullchain.pem',
        'ssl_certificate_key' => getenv('HOME') . '/.ssl/php-dev-router/privkey.pem',
        'php_fpm_socket' => '/run/php-fpm/php-fpm.sock',
        'nginx_conf' => '/etc/nginx/conf.d/php-dev-router.conf',
        'hosts_file' => '/etc/hosts',
        'systemd_service' => '/etc/systemd/system/php-dev-router.service',
        'skip_dirs' => ['.git', '.idea', '.vscode', 'bootstrap/cache', 'node_modules', 'storage', 'vendor'],
        'scan_max_depth' => 5,
        'watch_interval_seconds' => 3,
        'config_path' => $configPath,
        'project_root' => $projectRoot,
    ];

    $config = array_replace($defaults, $config);
    foreach (['projects_root', 'ssl_certificate', 'ssl_certificate_key', 'nginx_conf', 'hosts_file', 'systemd_service'] as $key) {
        $config[$key] = expandPath((string) $config[$key]);
    }
    $config['domain'] = trim((string) $config['domain'], ". \t\n\r\0\x0B");
    $config['scan_max_depth'] = max(1, (int) $config['scan_max_depth']);
    $config['watch_interval_seconds'] = max(1, (int) $config['watch_interval_seconds']);

    return $config;
}

function commandHelp(): void
{
    $version = VERSION;
    echo <<<TEXT
php-dev-router v{$version}

Usage:
  php-dev-router scan [--json]
  php-dev-router apply [--dry-run] [--no-reload] [--no-nginx-test]
  php-dev-router watch [--interval=3]
  php-dev-router doctor
  php-dev-router install [--dry-run]

Commands:
  scan      Discover PHP/Laravel apps and show hostnames.
  apply     Generate nginx config and exact local hosts entries.
  watch     Re-apply when the project inventory changes.
  doctor    Check config, certs, services, generated files, and DNS.
  install   Create the systemd service and run apply.

TEXT;
}

function commandScan(array $config, array $options): void
{
    $apps = discoverApps($config);
    if (isset($options['json'])) {
        echo json_encode($apps, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
        return;
    }

    if ($apps === []) {
        echo "No serveable PHP apps found in {$config['projects_root']}\n";
        return;
    }

    foreach ($apps as $app) {
        echo "{$app['host']} -> {$app['root']} ({$app['type']})\n";
    }
}

function commandApply(array $config, array $options): void
{
    $apps = discoverApps($config);
    $nginx = renderNginx($config, $apps);
    $hosts = renderHostsBlock($apps);

    if (isset($options['dry-run'])) {
        echo "Would write nginx config: {$config['nginx_conf']}\n";
        echo "Would update hosts file: {$config['hosts_file']}\n\n";
        echo $nginx;
        echo "\n";
        echo $hosts;
        return;
    }

    ensureWritableTargets([$config['nginx_conf'], $config['hosts_file']]);
    atomicWrite($config['nginx_conf'], $nginx);
    updateHostsFile($config['hosts_file'], $hosts);

    if (empty($options['no-nginx-test'])) {
        runOrFail(['nginx', '-t']);
    }
    if (empty($options['no-reload'])) {
        reloadService('nginx');
    }

    echo "Applied " . count($apps) . " app route(s).\n";
}

function commandWatch(array $config, array $options): void
{
    $interval = isset($options['interval']) ? max(1, (int) $options['interval']) : $config['watch_interval_seconds'];
    echo "Watching {$config['projects_root']} every {$interval}s\n";

    $lastHash = '';
    while (true) {
        $apps = discoverApps($config);
        $hash = hash('sha256', json_encode($apps, JSON_UNESCAPED_SLASHES));
        if ($hash !== $lastHash) {
            $lastHash = $hash;
            try {
                commandApply($config, ['no-reload' => false]);
            } catch (RuntimeException $exception) {
                fwrite(STDERR, date('c') . " apply failed: {$exception->getMessage()}\n");
            }
        }
        sleep($interval);
    }
}

function commandDoctor(array $config): void
{
    $apps = discoverApps($config);
    $checks = [
        ['Projects root', is_dir($config['projects_root']), $config['projects_root']],
        ['SSL cert', is_file($config['ssl_certificate']), $config['ssl_certificate']],
        ['SSL key', is_file($config['ssl_certificate_key']), $config['ssl_certificate_key']],
        ['PHP-FPM socket', file_exists($config['php_fpm_socket']), $config['php_fpm_socket']],
        ['nginx config dir', is_dir(dirname($config['nginx_conf'])), dirname($config['nginx_conf'])],
        ['hosts file', is_file($config['hosts_file']), $config['hosts_file']],
    ];

    foreach ($checks as [$name, $ok, $detail]) {
        echo sprintf("%-18s %s %s\n", $name, $ok ? 'OK  ' : 'MISS', $detail);
    }

    echo "\nDetected apps: " . count($apps) . "\n";
    foreach ($apps as $app) {
        $resolved = trim((string) shell_exec('getent hosts ' . escapeshellarg($app['host']) . ' 2>/dev/null'));
        $dnsStatus = str_contains($resolved, '127.0.0.1') ? 'resolves' : 'not-local';
        echo "  {$app['host']} -> {$app['root']} ({$dnsStatus})\n";
    }
}

function commandInstall(array $config, array $options): void
{
    $service = renderSystemdService($config);

    if (isset($options['dry-run'])) {
        echo "Would write systemd service: {$config['systemd_service']}\n\n";
        echo $service;
        echo "\n";
        commandApply($config, ['dry-run' => true]);
        return;
    }

    ensureWritableTargets([$config['systemd_service'], $config['nginx_conf'], $config['hosts_file']]);
    atomicWrite($config['systemd_service'], $service);
    commandApply($config, ['no-reload' => false]);

    runOrFail(['systemctl', 'daemon-reload']);
    enableServiceIfAvailable('php-fpm');
    enableServiceIfAvailable('nginx');
    runOrFail(['systemctl', 'enable', '--now', 'php-dev-router.service']);

    echo "Installed php-dev-router watcher service.\n";
}

function discoverApps(array $config): array
{
    $root = rtrim($config['projects_root'], '/');
    if (!is_dir($root)) {
        return [];
    }

    $candidates = [];
    scanDirectory($root, $root, 0, $config, $candidates);

    $usedHosts = [];
    $apps = [];
    foreach ($candidates as $candidate) {
        $relative = trim(substr($candidate['app_dir'], strlen($root)), '/');
        $slug = slugify($relative === '' ? basename($candidate['app_dir']) : $relative);
        $host = "{$slug}.{$config['domain']}";
        if (isset($usedHosts[$host])) {
            $hash = substr(hash('sha1', $candidate['app_dir']), 0, 7);
            $host = "{$slug}-{$hash}.{$config['domain']}";
        }
        $usedHosts[$host] = true;
        $apps[] = [
            'host' => $host,
            'name' => $slug,
            'type' => $candidate['type'],
            'app_dir' => $candidate['app_dir'],
            'root' => $candidate['root'],
            'index' => $candidate['root'] . '/index.php',
        ];
    }

    usort($apps, fn (array $a, array $b): int => strcmp($a['host'], $b['host']));
    return $apps;
}

function scanDirectory(string $dir, string $base, int $depth, array $config, array &$candidates): void
{
    if ($depth > $config['scan_max_depth']) {
        return;
    }

    $dir = rtrim($dir, '/');
    if (is_file("{$dir}/public/index.php")) {
        $type = is_file("{$dir}/artisan") ? 'laravel' : 'public-front-controller';
        $candidates[] = ['app_dir' => $dir, 'root' => "{$dir}/public", 'type' => $type];
    } elseif (is_file("{$dir}/index.php")) {
        $candidates[] = ['app_dir' => $dir, 'root' => $dir, 'type' => 'generic-php'];
    }

    $children = @scandir($dir);
    if ($children === false) {
        return;
    }

    foreach ($children as $child) {
        if ($child === '.' || $child === '..') {
            continue;
        }

        $path = "{$dir}/{$child}";
        if ($child === 'public' || str_starts_with($child, '.')) {
            continue;
        }
        if (!is_dir($path) || shouldSkip($path, $base, $config['skip_dirs'])) {
            continue;
        }
        scanDirectory($path, $base, $depth + 1, $config, $candidates);
    }
}

function shouldSkip(string $path, string $base, array $skipDirs): bool
{
    $relative = trim(substr($path, strlen(rtrim($base, '/'))), '/');
    $name = basename($path);
    foreach ($skipDirs as $skip) {
        $skip = trim((string) $skip, '/');
        if ($skip === $name || $skip === $relative || str_ends_with($relative, "/{$skip}")) {
            return true;
        }
    }

    return false;
}

function renderNginx(array $config, array $apps): string
{
    $lines = [
        '# Managed by php-dev-router. Re-running apply replaces this file.',
        '# Source: ' . $config['config_path'],
        '',
    ];

    foreach ($apps as $app) {
        $host = nginxEscape($app['host']);
        $root = nginxEscape($app['root']);
        $cert = nginxEscape($config['ssl_certificate']);
        $key = nginxEscape($config['ssl_certificate_key']);
        $socket = nginxEscape($config['php_fpm_socket']);

        $lines[] = "server {";
        $lines[] = "    listen 80;";
        $lines[] = "    server_name {$host};";
        $lines[] = "    return 301 https://{$host}\$request_uri;";
        $lines[] = "}";
        $lines[] = "";
        $lines[] = "server {";
        $lines[] = "    listen 443 ssl;";
        $lines[] = "    server_name {$host};";
        $lines[] = "";
        $lines[] = "    ssl_certificate \"{$cert}\";";
        $lines[] = "    ssl_certificate_key \"{$key}\";";
        $lines[] = "";
        $lines[] = "    root \"{$root}\";";
        $lines[] = "    index index.php index.html;";
        $lines[] = "";
        $lines[] = "    client_max_body_size 128m;";
        $lines[] = "";
        $lines[] = "    location / {";
        $lines[] = "        try_files \$uri \$uri/ /index.php?\$query_string;";
        $lines[] = "    }";
        $lines[] = "";
        $lines[] = "    location ~ \\.php$ {";
        $lines[] = "        try_files \$uri =404;";
        $lines[] = "        fastcgi_pass unix:{$socket};";
        $lines[] = "        fastcgi_index index.php;";
        $lines[] = "        include fastcgi.conf;";
        $lines[] = "    }";
        $lines[] = "";
        $lines[] = "    location ~ /\\.(?!well-known).* {";
        $lines[] = "        deny all;";
        $lines[] = "    }";
        $lines[] = "}";
        $lines[] = "";
    }

    return implode("\n", $lines);
}

function renderHostsBlock(array $apps): string
{
    $lines = [HOSTS_BEGIN];
    foreach ($apps as $app) {
        $lines[] = "127.0.0.1 {$app['host']}";
    }
    $lines[] = HOSTS_END;
    $lines[] = '';
    return implode("\n", $lines);
}

function updateHostsFile(string $path, string $block): void
{
    $current = is_file($path) ? (string) file_get_contents($path) : '';
    $pattern = '/' . preg_quote(HOSTS_BEGIN, '/') . '.*?' . preg_quote(HOSTS_END, '/') . "\\n?/s";
    if (preg_match($pattern, $current)) {
        $new = preg_replace($pattern, $block, $current);
    } else {
        $new = rtrim($current) . "\n\n" . $block;
    }

    atomicWrite($path, (string) $new);
}

function renderSystemdService(array $config): string
{
    $php = trim((string) shell_exec('command -v php 2>/dev/null')) ?: '/usr/bin/php';
    $script = __FILE__;
    $configPath = $config['config_path'];
    $interval = $config['watch_interval_seconds'];

    return <<<UNIT
[Unit]
Description=PHP development router
After=network.target nginx.service php-fpm.service
Wants=nginx.service php-fpm.service

[Service]
Type=simple
ExecStart={$php} {$script} watch --config={$configPath} --interval={$interval}
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

UNIT;
}

function defaultConfigPath(string $projectRoot): string
{
    $localConfig = "{$projectRoot}/config.json";
    if (is_file($localConfig)) {
        return $localConfig;
    }

    return "{$projectRoot}/config.example.json";
}

function atomicWrite(string $path, string $contents): void
{
    $dir = dirname($path);
    if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
        fail("Could not create directory: {$dir}");
    }

    $mode = file_exists($path) ? (fileperms($path) & 0777) : 0644;
    $owner = file_exists($path) ? fileowner($path) : false;
    $group = file_exists($path) ? filegroup($path) : false;
    $tmp = tempnam($dir, '.php-dev-router.');
    if ($tmp === false) {
        fail("Could not create temp file in {$dir}");
    }
    if (file_put_contents($tmp, $contents) === false) {
        @unlink($tmp);
        fail("Could not write temp file: {$tmp}");
    }
    @chmod($tmp, $mode);
    if ($owner !== false) {
        @chown($tmp, $owner);
    }
    if ($group !== false) {
        @chgrp($tmp, $group);
    }
    if (!rename($tmp, $path)) {
        @unlink($tmp);
        fail("Could not replace file: {$path}");
    }
    @chmod($path, $mode);
}

function runOrFail(array $command): void
{
    $escaped = array_map('escapeshellarg', $command);
    $cmd = implode(' ', $escaped) . ' 2>&1';
    exec($cmd, $output, $code);
    if ($code !== 0) {
        fail("Command failed ({$code}): " . implode(' ', $command) . "\n" . implode("\n", $output));
    }
}

function reloadService(string $service): void
{
    exec('systemctl reload ' . escapeshellarg($service) . ' 2>&1', $output, $code);
    if ($code === 0) {
        return;
    }

    exec('systemctl restart ' . escapeshellarg($service) . ' 2>&1', $restartOutput, $restartCode);
    if ($restartCode !== 0) {
        fail("Could not reload or restart {$service}\n" . implode("\n", array_merge($output, $restartOutput)));
    }
}

function enableServiceIfAvailable(string $service): void
{
    exec('systemctl list-unit-files ' . escapeshellarg($service . '.service') . ' --no-legend 2>/dev/null', $output, $code);
    if ($code === 0 && trim(implode("\n", $output)) !== '') {
        runOrFail(['systemctl', 'enable', '--now', $service . '.service']);
    }
}

function ensureWritableTargets(array $paths): void
{
    $blocked = [];
    foreach ($paths as $path) {
        $parent = dirname($path);
        if ((is_file($path) && is_writable($path)) || (!file_exists($path) && is_dir($parent) && is_writable($parent))) {
            continue;
        }
        $blocked[] = $path;
    }

    if ($blocked !== []) {
        fail("This command cannot write required target(s). Re-run with sudo.\nTargets: " . implode(', ', $blocked));
    }
}

function expandPath(string $path): string
{
    if ($path === '~') {
        return (string) getenv('HOME');
    }
    if (str_starts_with($path, '~/')) {
        return getenv('HOME') . substr($path, 1);
    }
    return $path;
}

function slugify(string $value): string
{
    $value = strtolower($value);
    $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? $value;
    $value = trim($value, '-');
    return $value !== '' ? $value : 'app';
}

function nginxEscape(string $value): string
{
    return str_replace(['\\', '"'], ['\\\\', '\\"'], $value);
}

function fail(string $message): never
{
    throw new RuntimeException($message);
}
