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

declare(strict_types=1);

/**
 * audit-di-collisions.php
 *
 * Audits a project's PHP-DI definition files for key collisions between host
 * modules and vendor packages, ahead of the apexl/io 3.9.0 behaviour change in
 * which host `module/*\/services.php` definitions override vendor package
 * definitions (previously vendor silently won). See
 * Plans/host-overrides-vendor-di.md.
 *
 * Usage:
 *   php bin/audit-di-collisions.php /path/to/project/api
 *
 * It enumerates the SAME two globs as
 * apexl\Io\factories\ContainerFactory::moduleDefinitions():
 *   - <project>/module/*\/services.php                       (HOST)
 *   - <project>/vendor/apexl/**\/services.php                (VENDOR, via Globstar)
 *
 * and reports:
 *   - HOST <-> VENDOR duplicate keys  — the behaviour-change set. Under 3.9.0
 *     the host file now wins where previously the vendor file did.
 *   - VENDOR <-> VENDOR duplicate keys — informational only; relative order
 *     within the vendor tier is unchanged by 3.9.0.
 *
 * Exit code: 0 when there are no host<->vendor collisions, 1 otherwise.
 *
 * Dependency-free beyond the audited project's own Composer autoloader.
 */

use apexl\Utils\Paths\Globstar;

if ($argc < 2) {
    fwrite(STDERR, "Usage: php bin/audit-di-collisions.php /path/to/project/api\n");
    exit(2);
}

// Normalise the project directory (accept Windows or POSIX separators, strip a
// trailing slash). glob()/require handle forward slashes and spaces on Windows.
$projectDir = rtrim(str_replace('\\', '/', $argv[1]), '/');

$resolved = realpath($projectDir);
if ($resolved === false) {
    fwrite(STDERR, "ERROR: project directory not found: {$projectDir}\n");
    exit(2);
}
$projectDir = str_replace('\\', '/', $resolved);

$autoloadPath = $projectDir . '/vendor/autoload.php';
if (!is_file($autoloadPath)) {
    fwrite(STDERR, "ERROR: composer autoloader not found at {$autoloadPath}\n");
    exit(2);
}
require $autoloadPath;

if (!class_exists(Globstar::class)) {
    fwrite(STDERR, "ERROR: apexl\\Utils\\Paths\\Globstar not resolvable from the project autoloader.\n");
    exit(2);
}

/**
 * Load a services.php file and return its definition keys.
 *
 * Definition files are side-effect-free but DO reference enum/class constants
 * and DI helper functions supplied by the project autoloader, so requiring them
 * can throw an Error (missing class/const) as well as an Exception. Both are
 * caught so a single bad file does not abort the whole audit.
 *
 * @return array{keys: list<int|string>, error: ?string}
 */
function loadDefinitionKeys(string $file): array
{
    try {
        $definitions = require $file;

        if (!is_array($definitions)) {
            return ['keys' => [], 'error' => 'file did not return an array'];
        }

        return ['keys' => array_keys($definitions), 'error' => null];
    } catch (\Throwable $e) {
        return ['keys' => [], 'error' => $e::class . ': ' . $e->getMessage()];
    }
}

$hostFiles   = glob($projectDir . '/module/*/services.php') ?: [];
$vendorFiles = Globstar::globStar($projectDir . '/vendor/apexl/**/services.php') ?: [];

sort($hostFiles);
sort($vendorFiles);

// key => list of ['file' => path, 'tier' => 'host'|'vendor']
$keyOwners = [];
$loadErrors = [];

foreach ([['host', $hostFiles], ['vendor', $vendorFiles]] as [$tier, $files]) {
    foreach ($files as $file) {
        $normFile = str_replace('\\', '/', $file);
        $result = loadDefinitionKeys($file);

        if ($result['error'] !== null) {
            $loadErrors[] = ['file' => $normFile, 'tier' => $tier, 'error' => $result['error']];
            continue;
        }

        foreach ($result['keys'] as $key) {
            $keyOwners[$key][] = ['file' => $normFile, 'tier' => $tier];
        }
    }
}

// Classify collisions.
$hostVendorCollisions = []; // key => ['host' => [files], 'vendor' => [files]]
$vendorVendorCollisions = []; // key => [files]

foreach ($keyOwners as $key => $owners) {
    $hostFilesForKey = [];
    $vendorFilesForKey = [];

    foreach ($owners as $owner) {
        if ($owner['tier'] === 'host') {
            $hostFilesForKey[] = $owner['file'];
        } else {
            $vendorFilesForKey[] = $owner['file'];
        }
    }

    if ($hostFilesForKey !== [] && $vendorFilesForKey !== []) {
        $hostVendorCollisions[$key] = [
            'host' => $hostFilesForKey,
            'vendor' => $vendorFilesForKey,
        ];
    }

    if (count($vendorFilesForKey) > 1) {
        $vendorVendorCollisions[$key] = $vendorFilesForKey;
    }
}

// Report.
$line = str_repeat('=', 72);
echo $line . "\n";
echo "DI definition collision audit\n";
echo "Project: {$projectDir}\n";
echo $line . "\n";
echo 'Host services.php files:   ' . count($hostFiles) . "\n";
echo 'Vendor services.php files: ' . count($vendorFiles) . "\n";
echo 'Distinct definition keys:  ' . count($keyOwners) . "\n";
echo "\n";

if ($loadErrors !== []) {
    echo "LOAD FAILURES (reported, not fatal):\n";
    foreach ($loadErrors as $err) {
        echo "  [{$err['tier']}] {$err['file']}\n";
        echo "      {$err['error']}\n";
    }
    echo "\n";
}

echo "HOST <-> VENDOR collisions (BEHAVIOUR CHANGE — host now wins under 3.9.0):\n";
if ($hostVendorCollisions === []) {
    echo "  none\n";
} else {
    foreach ($hostVendorCollisions as $key => $sides) {
        echo "  key: {$key}\n";
        foreach ($sides['host'] as $f) {
            echo "      HOST   (now wins): {$f}\n";
        }
        foreach ($sides['vendor'] as $f) {
            echo "      VENDOR (was wins): {$f}\n";
        }
    }
}
echo "\n";

echo "VENDOR <-> VENDOR duplicate keys (informational — order unchanged by 3.9.0):\n";
if ($vendorVendorCollisions === []) {
    echo "  none\n";
} else {
    foreach ($vendorVendorCollisions as $key => $files) {
        echo "  key: {$key}\n";
        foreach ($files as $f) {
            echo "      VENDOR: {$f}\n";
        }
    }
}
echo "\n";

$hostVendorCount = count($hostVendorCollisions);
if ($hostVendorCount === 0) {
    echo "RESULT: no host<->vendor collisions. Safe to upgrade to apexl/io 3.9.0.\n";
    exit(0);
}

echo "RESULT: {$hostVendorCount} host<->vendor collision(s) found. Review each before upgrading.\n";
exit(1);
