<?php
declare(strict_types=1);

require_once __DIR__ . '/StayIngestionService.php';
require_once __DIR__ . '/StayProcessingWriter.php';
require_once __DIR__ . '/StayProcessingSelector.php';
require_once __DIR__ . '/StayProcessingEnrichmentWorker.php';
require_once __DIR__ . '/StayProcessingGeographyWorker.php';
require_once __DIR__ . '/AnchorageGeocoder.php';

/**
 * YachtMemory StayProcessingService - Phase 2A.5a.
 *
 * Central orchestration for one vessel:
 *
 * 1. detect/persist incremental Stay changes
 * 2. reconcile persistent processing state
 * 3. inspect/run due Enrichment work
 * 4. inspect/run due Geography work
 *
 * Enrichment and Geography are deliberately separate processing dimensions:
 *
 * - Enrichment belongs to a particular Stay revision.
 * - Geography belongs to the stable Stay identity (stay_id).
 *
 * They therefore run sequentially here for deterministic orchestration, but
 * Geography does not depend on successful Enrichment and must not inherit
 * Enrichment's revision semantics.
 *
 * DRY_RUN remains strictly read only:
 *
 * - Stay ingestion is planned but not applied.
 * - processing reconciliation is planned but not applied.
 * - neither Enrichment nor Geography worker is run.
 * - due work for both dimensions is reported from the current database state.
 *
 * Geography acquisition is deliberately CACHE_ONLY in this integration phase.
 * Enabling network acquisition is a separate operational decision after the
 * integrated service path has been verified.
 *
 * Operational concerns such as enabled gating, shared locking, cadence and run
 * history remain responsibilities of the surrounding manual/scheduled runners.
 */
final class StayProcessingService
{
    /**
     * Run one complete Stay Processing cycle for a vessel.
     *
     * The existing $enrichmentLimit remains the common per-phase worker limit
     * for this integration step. A separate Geography limit is not introduced
     * until an operational requirement justifies another configuration value.
     *
     * @return array<string,mixed>
     */
    public static function runForVessel(
        int $vesselId,
        int $enrichmentLimit = 20,
        int $retryMinutes = 30,
        bool $dryRun = false,
        ?string $now = null
    ): array {
        if ($vesselId <= 0) {
            throw new InvalidArgumentException(
                'vesselId must be greater than zero.'
            );
        }

        if ($enrichmentLimit < 1) {
            throw new InvalidArgumentException(
                'enrichmentLimit must be greater than zero.'
            );
        }

        if ($retryMinutes < 1) {
            throw new InvalidArgumentException(
                'retryMinutes must be greater than zero.'
            );
        }

        $startedAt = date('Y-m-d H:i:s');

        /*
         * Ingestion must run before reconciliation in APPLY mode. A changed Stay
         * revision then becomes visible to StayProcessingWriter and starts a
         * fresh Enrichment cycle.
         *
         * Geography differs deliberately: reconciliation must not restart
         * Geography merely because an OPEN Stay received another revision.
         *
         * In DRY_RUN, ingestion only plans the prospective Stay change. We do
         * not simulate that future state in stay_processing; all due counts
         * therefore describe the database exactly as it exists now.
         */
        $ingestion = StayIngestionService::runForVessel(
            $vesselId,
            $dryRun
        );

        $plan = StayProcessingWriter::planForVessel($vesselId);
        $planSummary = self::summarizePlan($plan);

        if ($dryRun) {
            $dueEnrichment = StayProcessingSelector::selectDueEnrichment(
                $enrichmentLimit,
                $now,
                $vesselId
            );

            $dueGeography = StayProcessingSelector::selectDueGeography(
                $enrichmentLimit,
                $now,
                $vesselId
            );

            return [
                'mode' => 'DRY_RUN',
                'vessel_id' => $vesselId,
                'started_at' => $startedAt,
                'finished_at' => date('Y-m-d H:i:s'),
                'stay_ingestion' => $ingestion,
                'reconciliation_plan' => $planSummary,
                'reconciliation_apply' => null,
                'due_enrichment_before_worker' => count($dueEnrichment),
                'enrichment_worker' => null,
                'due_geography_before_worker' => count($dueGeography),
                'geography_worker' => null,
            ];
        }

        $apply = StayProcessingWriter::apply($plan);

        /*
         * Selection happens after reconciliation because newly persisted Stays
         * or revisions may have created processing work.
         *
         * The explicit pre-worker counts are diagnostic snapshots. Each worker
         * performs its own guarded selection/claim and remains authoritative
         * about what it actually processed.
         */
        $dueEnrichment = StayProcessingSelector::selectDueEnrichment(
            $enrichmentLimit,
            $now,
            $vesselId
        );

        $enrichmentWorker = StayProcessingEnrichmentWorker::runDue(
            $enrichmentLimit,
            $now,
            $vesselId,
            $retryMinutes
        );

        /*
         * Geography is orchestrated after Enrichment only to keep one service
         * run deterministic. There is intentionally no dependency check on the
         * Enrichment result.
         *
         * CACHE_ONLY is explicit here rather than relying solely on the worker
         * default. This makes the operational safety boundary visible at the
         * orchestration layer that will later decide when network acquisition
         * may be enabled.
         */
        $dueGeography = StayProcessingSelector::selectDueGeography(
            $enrichmentLimit,
            $now,
            $vesselId
        );

        $geographyWorker = StayProcessingGeographyWorker::runDue(
            $enrichmentLimit,
            $now,
            $vesselId,
            $retryMinutes,
            AnchorageGeocoder::ACQUISITION_CACHE_ONLY
        );

        return [
            'mode' => 'APPLY',
            'vessel_id' => $vesselId,
            'started_at' => $startedAt,
            'finished_at' => date('Y-m-d H:i:s'),
            'stay_ingestion' => $ingestion,
            'reconciliation_plan' => $planSummary,
            'reconciliation_apply' => $apply,
            'due_enrichment_before_worker' => count($dueEnrichment),
            'enrichment_worker' => $enrichmentWorker,
            'due_geography_before_worker' => count($dueGeography),
            'geography_worker' => $geographyWorker,
        ];
    }

    /**
     * Reduce the reconciliation plan to the operational counters exposed by the
     * service and consumed by run-history logging.
     *
     * @return array{total:int,insert:int,update:int,unchanged:int}
     */
    private static function summarizePlan(array $plan): array
    {
        $summary = [
            'total' => count($plan),
            'insert' => 0,
            'update' => 0,
            'unchanged' => 0,
        ];

        foreach ($plan as $item) {
            $action = strtoupper((string)($item['action'] ?? ''));

            if ($action === 'INSERT') {
                $summary['insert']++;
            } elseif ($action === 'UPDATE') {
                $summary['update']++;
            } elseif ($action === 'UNCHANGED') {
                $summary['unchanged']++;
            } else {
                throw new RuntimeException(
                    'Unexpected reconciliation action: ' . $action
                );
            }
        }

        return $summary;
    }
}
