<?php

declare(strict_types=1);

namespace App\App;

use App\Application\Admin\AllowlistController;
use App\Application\Admin\AppSettingsController;
use App\Application\Admin\AuditController;
use App\Application\Admin\CategoriesController;
use App\Application\Admin\ConfigController;
use App\Application\Admin\ConsumersController;
use App\Application\Admin\IpsController;
use App\Application\Admin\JobsAdminController;
use App\Application\Admin\MaintenanceController;
use App\Application\Admin\ManualBlocksController;
use App\Application\Admin\MeController;
use App\Application\Admin\PoliciesController;
use App\Application\Admin\ReportersController;
use App\Application\Admin\StatsController;
use App\Application\Admin\TokensController;
use App\Application\Admin\UsersController;
use App\Application\Auth\AuthController;
use App\Application\Internal\JobsController;
use App\Application\Jobs\CleanupAuditJob;
use App\Application\Jobs\CleanupExpiredManualBlocksJob;
use App\Application\Jobs\EnrichPendingJob;
use App\Application\Jobs\RecomputeScoresJob;
use App\Application\Jobs\RefreshGeoipJob;
use App\Application\Jobs\TickJob;
use App\Application\Public\BlocklistController;
use App\Application\Public\DocsController;
use App\Application\Public\ReportController;
use App\Domain\Audit\AuditEmitter;
use App\Domain\Auth\Role;
use App\Domain\Auth\TokenHasher;
use App\Domain\Auth\TokenIssuer;
use App\Domain\Enrichment\EnrichmentService;
use App\Domain\Reputation\BlocklistBuilder;
use App\Domain\Reputation\EffectiveStatusService;
use App\Domain\Reputation\PairScorer;
use App\Domain\Settings\AppSettings;
use App\Domain\Time\Clock;
use App\Domain\Time\SystemClock;
use App\Infrastructure\Allowlist\AllowlistRepository;
use App\Infrastructure\Audit\AuditRepository;
use App\Infrastructure\Audit\DbAuditEmitter;
use App\Infrastructure\Auth\RoleMappingRepository;
use App\Infrastructure\Auth\ServiceTokenBootstrap;
use App\Infrastructure\Auth\TokenRepository;
use App\Infrastructure\Auth\UserRepository;
use App\Infrastructure\Category\CategoryRepository;
use App\Infrastructure\Consumer\ConsumerRepository;
use App\Infrastructure\Db\ConnectionFactory;
use App\Infrastructure\Enrichment\Downloaders\DbipDownloader;
use App\Infrastructure\Enrichment\Downloaders\GeoIpDownloader;
use App\Infrastructure\Enrichment\Downloaders\IPinfoDownloader;
use App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader;
use App\Infrastructure\Enrichment\IpinfoRecordAdapter;
use App\Infrastructure\Enrichment\MaxMindRecordAdapter;
use App\Infrastructure\Enrichment\MmdbEnrichmentService;
use App\Infrastructure\Enrichment\RecordAdapter;
use App\Infrastructure\Http\JsonErrorHandler;
use App\Infrastructure\Http\Middleware\AuditContextMiddleware;
use App\Infrastructure\Http\Middleware\ImpersonationMiddleware;
use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
use App\Infrastructure\Http\Middleware\InternalTokenMiddleware;
use App\Infrastructure\Http\Middleware\RateLimitMiddleware;
use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
use App\Infrastructure\Http\RateLimiter;
use App\Infrastructure\Jobs\JobLockRepository;
use App\Infrastructure\Jobs\JobRegistry;
use App\Infrastructure\Jobs\JobRunner;
use App\Infrastructure\Jobs\JobRunRepository;
use App\Infrastructure\ManualBlock\ManualBlockRepository;
use App\Infrastructure\Policy\PolicyRepository;
use App\Infrastructure\Reporter\ReporterRepository;
use App\Infrastructure\Reputation\BlocklistCache;
use App\Infrastructure\Reputation\CidrEvaluatorFactory;
use App\Infrastructure\Reputation\DashboardStatsRepository;
use App\Infrastructure\Reputation\IpEnrichmentRepository;
use App\Infrastructure\Reputation\IpHistoryRepository;
use App\Infrastructure\Reputation\IpScoreRepository;
use App\Infrastructure\Reputation\ReportRepository;
use App\Infrastructure\Settings\DbAppSettings;

use function DI\autowire;

use DI\ContainerBuilder;

use function DI\factory;

use Doctrine\DBAL\Connection;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Log\LoggerInterface;
use Slim\Psr7\Factory\ResponseFactory;

/**
 * Builds the api's DI container.
 *
 * M04 additions: ReporterRepository / ConsumerRepository / CategoryRepository /
 * ReportRepository / IpScoreRepository, the PairScorer + Clock pair, and the
 * RateLimiter singleton plus its middleware. The rate-limit settings flow
 * through `settings` so tests can override capacity/refill cleanly.
 */
final class Container
{
    /**
     * @param array<string, mixed>|null $settings Optional override (tests pass in fixtures).
     */
    public static function build(?array $settings = null): ContainerInterface
    {
        $settings ??= require __DIR__ . '/../../config/settings.php';

        $builder = new ContainerBuilder();
        $builder->useAutowiring(true);
        $builder->addDefinitions([
            'settings' => $settings,
            'settings.db' => $settings['db'],
            'settings.ui_service_token' => $settings['ui_service_token'] ?? '',
            'settings.app_env' => $settings['app_env'] ?? 'production',
            'settings.log_level' => $settings['log_level'] ?? \Monolog\Level::Info,
            'settings.oidc_default_role' => $settings['oidc_default_role'] ?? Role::Viewer,
            'settings.score_hard_cutoff_days' => (int) ($settings['score_hard_cutoff_days'] ?? 365),
            'settings.rate_limit_per_second' => (int) ($settings['rate_limit_per_second'] ?? 60),
            'settings.internal_job_token' => (string) ($settings['internal_job_token'] ?? ''),
            'settings.internal_cidr_allowlist' => (string) ($settings['internal_cidr_allowlist'] ?? ''),
            'settings.api_docs_public' => (bool) ($settings['api_docs_public'] ?? false),
            'settings.job_recompute_max_runtime_seconds' => (int) ($settings['job_recompute_max_runtime_seconds'] ?? 240),
            'settings.job_recompute_max_rows_per_tick' => (int) ($settings['job_recompute_max_rows_per_tick'] ?? 5000),
            'settings.job_audit_retention_days' => (int) ($settings['job_audit_retention_days'] ?? 180),
            'settings.cidr_evaluator_ttl_seconds' => (int) ($settings['cidr_evaluator_ttl_seconds'] ?? 60),
            'settings.blocklist_cache_ttl_seconds' => (int) ($settings['blocklist_cache_ttl_seconds'] ?? 30),
            'settings.geoip' => $settings['geoip'] ?? [
                'enabled' => true,
                'provider' => 'dbip',
                'country_db' => '/data/geoip/country.mmdb',
                'asn_db' => '/data/geoip/asn.mmdb',
                'maxmind_license_key' => '',
                'ipinfo_token' => '',
                'refresh_interval_days' => 7,
            ],
            ConnectionFactory::class => factory(static function (ContainerInterface $c): ConnectionFactory {
                /** @var array{driver: string, sqlite_path: string, mysql_host: string, mysql_port: int, mysql_database: string, mysql_username: string, mysql_password: string} $db */
                $db = $c->get('settings.db');

                return new ConnectionFactory($db);
            }),
            Connection::class => factory(static function (ContainerInterface $c): Connection {
                /** @var ConnectionFactory $factory */
                $factory = $c->get(ConnectionFactory::class);

                return $factory->create();
            }),
            LoggerInterface::class => factory(static function (ContainerInterface $c): LoggerInterface {
                $logger = new Logger('api');
                /** @var \Monolog\Level $level */
                $level = $c->get('settings.log_level');
                $handler = new StreamHandler('php://stdout', $level);
                $handler->setFormatter(new JsonFormatter());
                $logger->pushHandler($handler);
                $logger->pushProcessor(new \App\Infrastructure\Logging\SecretScrubbingProcessor());

                return $logger;
            }),
            ResponseFactoryInterface::class => autowire(ResponseFactory::class),
            Clock::class => autowire(SystemClock::class),
            TokenHasher::class => autowire(),
            TokenIssuer::class => autowire(),
            TokenRepository::class => autowire(),
            RoleMappingRepository::class => autowire(),
            UserRepository::class => autowire(),
            ReporterRepository::class => autowire(),
            ConsumerRepository::class => autowire(),
            CategoryRepository::class => autowire(),
            ReportRepository::class => autowire(),
            IpScoreRepository::class => autowire(),
            ManualBlockRepository::class => autowire(),
            AllowlistRepository::class => autowire(),
            PolicyRepository::class => autowire(),
            IpEnrichmentRepository::class => autowire(),
            IpHistoryRepository::class => autowire(),
            DashboardStatsRepository::class => autowire(),
            CidrEvaluatorFactory::class => factory(static function (ContainerInterface $c): CidrEvaluatorFactory {
                /** @var ManualBlockRepository $manual */
                $manual = $c->get(ManualBlockRepository::class);
                /** @var AllowlistRepository $allow */
                $allow = $c->get(AllowlistRepository::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var LoggerInterface $logger */
                $logger = $c->get(LoggerInterface::class);
                /** @var int $ttl */
                $ttl = $c->get('settings.cidr_evaluator_ttl_seconds');

                return new CidrEvaluatorFactory($manual, $allow, $clock, $logger, $ttl);
            }),
            EffectiveStatusService::class => autowire(),
            BlocklistBuilder::class => autowire(),
            BlocklistCache::class => factory(static function (ContainerInterface $c): BlocklistCache {
                /** @var BlocklistBuilder $builder */
                $builder = $c->get(BlocklistBuilder::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var int $ttl */
                $ttl = $c->get('settings.blocklist_cache_ttl_seconds');

                return new BlocklistCache($builder, $clock, $ttl);
            }),
            ServiceTokenBootstrap::class => autowire(),
            TokenAuthenticationMiddleware::class => autowire(),
            ImpersonationMiddleware::class => autowire(),
            AuditContextMiddleware::class => autowire(),
            \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware::class => factory(static function (ContainerInterface $c): \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware {
                /** @var ResponseFactoryInterface $rf */
                $rf = $c->get(ResponseFactoryInterface::class);

                // SEC_REVIEW F69: 256 KiB global cap. Per-endpoint
                // caps (e.g. the 4 KiB `metadata` cap on /report)
                // layer on top.
                return new \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware($rf, 256 * 1024);
            }),
            AuditRepository::class => autowire(),
            AuditEmitter::class => autowire(DbAuditEmitter::class),
            AppSettings::class => autowire(DbAppSettings::class),
            PairScorer::class => factory(static function (ContainerInterface $c): PairScorer {
                /** @var ReportRepository $reports */
                $reports = $c->get(ReportRepository::class);
                /** @var CategoryRepository $categories */
                $categories = $c->get(CategoryRepository::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var int $cutoff */
                $cutoff = $c->get('settings.score_hard_cutoff_days');

                return new PairScorer($reports, $categories, $clock, $cutoff);
            }),
            RateLimiter::class => factory(static function (ContainerInterface $c): RateLimiter {
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var int $perSecond */
                $perSecond = $c->get('settings.rate_limit_per_second');
                $perSecond = max(1, $perSecond);

                return new RateLimiter($clock, (float) $perSecond, (float) ($perSecond * 2));
            }),
            RateLimitMiddleware::class => autowire(),
            JobLockRepository::class => autowire(),
            JobRunRepository::class => autowire(),
            JobRunner::class => factory(static function (ContainerInterface $c): JobRunner {
                /** @var JobLockRepository $locks */
                $locks = $c->get(JobLockRepository::class);
                /** @var JobRunRepository $runs */
                $runs = $c->get(JobRunRepository::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var LoggerInterface $logger */
                $logger = $c->get(LoggerInterface::class);

                return new JobRunner($locks, $runs, $clock, $logger);
            }),
            RecomputeScoresJob::class => factory(static function (ContainerInterface $c): RecomputeScoresJob {
                /** @var ReportRepository $reports */
                $reports = $c->get(ReportRepository::class);
                /** @var IpScoreRepository $ipScores */
                $ipScores = $c->get(IpScoreRepository::class);
                /** @var PairScorer $scorer */
                $scorer = $c->get(PairScorer::class);
                /** @var int $maxRuntime */
                $maxRuntime = $c->get('settings.job_recompute_max_runtime_seconds');
                /** @var int $maxRows */
                $maxRows = $c->get('settings.job_recompute_max_rows_per_tick');

                return new RecomputeScoresJob($reports, $ipScores, $scorer, $maxRuntime, $maxRows);
            }),
            CleanupAuditJob::class => factory(static function (ContainerInterface $c): CleanupAuditJob {
                /** @var Connection $conn */
                $conn = $c->get(Connection::class);
                /** @var int $days */
                $days = $c->get('settings.job_audit_retention_days');

                return new CleanupAuditJob($conn, $days);
            }),
            CleanupExpiredManualBlocksJob::class => autowire(),
            GuzzleClientInterface::class => factory(static function (): GuzzleClientInterface {
                // SEC_REVIEW F50: lock the GeoIP downloader's HTTP client
                // down. `allow_redirects` is now a tight whitelist —
                // HTTPS-only, ≤3 redirects, no Referer header. The
                // PrivateHostGuardMiddleware refuses to dial loopback /
                // link-local / RFC1918 / metadata-service hosts even
                // after a redirect rewrites the URL.
                $stack = \GuzzleHttp\HandlerStack::create();
                $stack->push(\App\Infrastructure\Enrichment\Downloaders\PrivateHostGuardMiddleware::factory());

                return new GuzzleClient([
                    'handler' => $stack,
                    'connect_timeout' => 10,
                    'timeout' => 120,
                    'http_errors' => true,
                    'allow_redirects' => [
                        'max' => 3,
                        'protocols' => ['https'],
                        'strict' => true,
                        'referer' => false,
                        'track_redirects' => false,
                    ],
                    'headers' => [
                        'User-Agent' => 'irdb-geoip-refresh/1.0',
                    ],
                ]);
            }),
            RecordAdapter::class => factory(static function (ContainerInterface $c): RecordAdapter {
                /** @var array{provider: string} $g */
                $g = $c->get('settings.geoip');

                return $g['provider'] === 'ipinfo'
                    ? new IpinfoRecordAdapter()
                    : new MaxMindRecordAdapter();
            }),
            MmdbEnrichmentService::class => factory(static function (ContainerInterface $c): MmdbEnrichmentService {
                /** @var array{country_db: string, asn_db: string} $g */
                $g = $c->get('settings.geoip');
                /** @var RecordAdapter $adapter */
                $adapter = $c->get(RecordAdapter::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);
                /** @var LoggerInterface $logger */
                $logger = $c->get(LoggerInterface::class);

                return new MmdbEnrichmentService($g['country_db'], $g['asn_db'], $adapter, $clock, $logger);
            }),
            EnrichmentService::class => factory(static function (ContainerInterface $c): EnrichmentService {
                /** @var MmdbEnrichmentService $svc */
                $svc = $c->get(MmdbEnrichmentService::class);

                return $svc;
            }),
            GeoIpDownloader::class => factory(static function (ContainerInterface $c): GeoIpDownloader {
                /** @var array{provider: string, maxmind_license_key: string, ipinfo_token: string} $g */
                $g = $c->get('settings.geoip');
                /** @var GuzzleClientInterface $http */
                $http = $c->get(GuzzleClientInterface::class);
                /** @var Clock $clock */
                $clock = $c->get(Clock::class);

                return match ($g['provider']) {
                    'maxmind' => new MaxMindDownloader($http, $g['maxmind_license_key']),
                    'ipinfo' => new IPinfoDownloader($http, $g['ipinfo_token']),
                    default => new DbipDownloader($http, $clock),
                };
            }),
            EnrichPendingJob::class => factory(static function (ContainerInterface $c): EnrichPendingJob {
                /** @var EnrichmentService $svc */
                $svc = $c->get(EnrichmentService::class);
                /** @var IpEnrichmentRepository $repo */
                $repo = $c->get(IpEnrichmentRepository::class);

                return new EnrichPendingJob($svc, $repo);
            }),
            RefreshGeoipJob::class => factory(static function (ContainerInterface $c): RefreshGeoipJob {
                /** @var GeoIpDownloader $downloader */
                $downloader = $c->get(GeoIpDownloader::class);
                /** @var MmdbEnrichmentService $svc */
                $svc = $c->get(MmdbEnrichmentService::class);
                /** @var IpEnrichmentRepository $repo */
                $repo = $c->get(IpEnrichmentRepository::class);
                /** @var array{country_db: string, asn_db: string, refresh_interval_days: int} $g */
                $g = $c->get('settings.geoip');

                return new RefreshGeoipJob(
                    $downloader,
                    $svc,
                    $repo,
                    $g['country_db'],
                    $g['asn_db'],
                    (int) $g['refresh_interval_days'],
                );
            }),
            TickJob::class => factory(static function (ContainerInterface $c): TickJob {
                /** @var JobRunner $runner */
                $runner = $c->get(JobRunner::class);
                /** @var JobRunRepository $runs */
                $runs = $c->get(JobRunRepository::class);

                // Closure indirection: TickJob iterates JobRegistry at
                // run() time, after the registry is fully populated. A
                // direct JobRegistry dependency would create a build-time
                // cycle (registry → tick → registry).
                $resolver = static fn (): array => $c->get(JobRegistry::class)->all();

                return new TickJob($resolver, $runner, $runs);
            }),
            JobRegistry::class => factory(static function (ContainerInterface $c): JobRegistry {
                $registry = new JobRegistry();
                /** @var RecomputeScoresJob $recompute */
                $recompute = $c->get(RecomputeScoresJob::class);
                /** @var CleanupAuditJob $cleanup */
                $cleanup = $c->get(CleanupAuditJob::class);
                /** @var CleanupExpiredManualBlocksJob $cleanupExpired */
                $cleanupExpired = $c->get(CleanupExpiredManualBlocksJob::class);
                /** @var EnrichPendingJob $enrich */
                $enrich = $c->get(EnrichPendingJob::class);
                /** @var RefreshGeoipJob $refresh */
                $refresh = $c->get(RefreshGeoipJob::class);
                /** @var TickJob $tick */
                $tick = $c->get(TickJob::class);
                $registry->register($recompute);
                $registry->register($cleanup);
                $registry->register($cleanupExpired);
                $registry->register($enrich);
                $registry->register($refresh);
                $registry->register($tick);

                return $registry;
            }),
            JobsController::class => autowire(),
            InternalNetworkMiddleware::class => factory(static function (ContainerInterface $c): InternalNetworkMiddleware {
                /** @var ResponseFactoryInterface $rf */
                $rf = $c->get(ResponseFactoryInterface::class);
                /** @var string $raw */
                $raw = $c->get('settings.internal_cidr_allowlist');

                return new InternalNetworkMiddleware($rf, InternalNetworkMiddleware::parseCidrList($raw));
            }),
            InternalTokenMiddleware::class => factory(static function (ContainerInterface $c): InternalTokenMiddleware {
                /** @var ResponseFactoryInterface $rf */
                $rf = $c->get(ResponseFactoryInterface::class);
                /** @var string $token */
                $token = $c->get('settings.internal_job_token');

                return new InternalTokenMiddleware($rf, $token);
            }),
            JsonErrorHandler::class => factory(static function (ContainerInterface $c): JsonErrorHandler {
                /** @var ResponseFactoryInterface $factory */
                $factory = $c->get(ResponseFactoryInterface::class);
                /** @var LoggerInterface $logger */
                $logger = $c->get(LoggerInterface::class);

                return new JsonErrorHandler(
                    $factory,
                    $logger,
                    $c->get('settings.app_env') === 'development',
                );
            }),
            AuthController::class => factory(static function (ContainerInterface $c): AuthController {
                /** @var Role|null $role */
                $role = $c->get('settings.oidc_default_role');
                /** @var UserRepository $users */
                $users = $c->get(UserRepository::class);
                /** @var AuditEmitter $audit */
                $audit = $c->get(AuditEmitter::class);
                /** @var Connection $conn */
                $conn = $c->get(Connection::class);

                return new AuthController($users, $role ?? Role::Viewer, $audit, $conn);
            }),
            MeController::class => autowire(),
            ReportersController::class => autowire(),
            ConsumersController::class => autowire(),
            TokensController::class => autowire(),
            ReportController::class => autowire(),
            ManualBlocksController::class => autowire(),
            AllowlistController::class => autowire(),
            PoliciesController::class => autowire(),
            BlocklistController::class => autowire(),
            IpsController::class => autowire(),
            StatsController::class => autowire(),
            CategoriesController::class => autowire(),
            UsersController::class => autowire(),
            AuditController::class => autowire(),
            JobsAdminController::class => autowire(),
            MaintenanceController::class => autowire(),
            AppSettingsController::class => autowire(),
            ConfigController::class => factory(static function (ContainerInterface $c): ConfigController {
                /** @var array<string, mixed> $settings */
                $settings = $c->get('settings');

                return new ConfigController($settings);
            }),
            DocsController::class => factory(static function (): DocsController {
                return new DocsController(__DIR__ . '/../../public/openapi.yaml');
            }),
        ]);

        return $builder->build();
    }
}
