<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Forms;

use Ekesto\CacheInvalidator\Integrations\Caching\AdapterManager as CacheAdapterManager;
use Ekesto\CacheInvalidator\Support\Debug;
use Ekesto\CacheInvalidator\Core\EmissionBuilder;
use Ekesto\CacheInvalidator\Core\EmissionHelper;
use Ekesto\CacheInvalidator\Core\GlobalInvalidationExecutor;
use Ekesto\CacheInvalidator\Integrations\Multilingual\LanguageResolverFactory;
use Ekesto\CacheInvalidator\InvalidatorRuntime;
use Ekesto\CacheInvalidator\Core\PurgeCoordinator;
use Ekesto\CacheInvalidator\Core\TrustDecider;

/**
 * Orchestrates forms-driven invalidation behavior.
 */
final class FormsInvalidationController
{
    private FormsUsageResolver $resolver;

    /** @var int Request-local sequence for forms change summaries. */
    private static int $eventSequence = 0;

    /**
     * Build controller with dependencies.
     *
     * @param FormsUsageResolver|null $resolver Resolver dependency.
     */
    public function __construct(?FormsUsageResolver $resolver = null)
    {
        $this->resolver = $resolver ?? new FormsUsageResolver();
    }

    /**
     * Handle a form change event from one adapter.
     *
     * @param FormAdapterInterface $adapter Adapter emitting event.
     * @param string|int           $formId Form ID.
     * @param array<string,mixed>  $context Event context.
     * @return void
     */
    public function handleFormChanged(FormAdapterInterface $adapter, $formId, array $context = []): void
    {
        $adapterKey = $adapter->key();
        $normalizedFormId = (string) $formId;
        $sourceHook = (string) ($context['source_hook'] ?? 'unknown');
        $event = (string) ($context['event'] ?? 'unknown');
        $eventSequence = ++self::$eventSequence;
        $eventKey = sprintf('%s|%s|%s|%s|%d', $adapterKey, $normalizedFormId, $sourceHook, $event, $eventSequence);

        $summary = [
            'event_sequence' => $eventSequence,
            'event_key' => $eventKey,
            'adapter' => $adapterKey,
            'form_id' => $normalizedFormId,
            'source_hook' => $sourceHook,
            'event' => $event,
            'outcome' => '',
            'references' => 0,
            'uncertain' => false,
            'uncertain_scan_gap' => false,
            'uncertain_meta_too_large' => false,
            'target_identifiers_count' => 0,
            'candidates_scanned' => 0,
            'candidates_total' => 0,
            'fallback_enabled' => false,
            'emitted_urls' => 0,
            'target_ids' => 0,
            'groups' => 0,
            'purged_urls_directly' => 0,
            'purge_skipped_urls' => 0,
            'intent' => 'presentation',
            'resolution' => '',
            'fallback' => false,
            'fallback_reason' => '',
            'target_identifiers' => [],
        ];

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            $summary['outcome'] = 'skipped_invalidation_disabled';
            $this->logSummary($summary);
            return;
        }

        $global = InvalidatorRuntime::settingsRepository()->getGlobalInvalidation();
        if (empty($global['forms_enabled'])) {
            $summary['outcome'] = 'skipped_forms_disabled';
            $this->logSummary($summary);
            return;
        }

        $cacheAdapter = CacheAdapterManager::getActiveAdapter();
        if ($cacheAdapter === null) {
            $summary['outcome'] = 'skipped_no_cache_adapter';
            $this->logSummary($summary);
            return;
        }

        if (GlobalInvalidationExecutor::hasGlobalInvalidationRun()) {
            $summary['outcome'] = 'skipped_terminal_global_already_run';
            $summary['resolution'] = 'skipped';
            $summary['fallback'] = false;
            $summary['fallback_reason'] = 'terminal_global_already_run';
            $this->logSummary($summary);
            return;
        }

        $resolution = $this->resolver->resolveReferencedPostIds($adapter, $normalizedFormId, $context);
        $resolutionPayload = $resolution->toArray();
        $summary['references'] = count($resolution->postIds());
        $summary['uncertain'] = $resolution->uncertain();
        $summary['uncertain_scan_gap'] = $resolution->uncertainDueToScanGap();
        $summary['uncertain_meta_too_large'] = $resolution->uncertainDueToMetaTooLarge();
        $summary['target_identifiers_count'] = count($resolutionPayload['target_identifiers'] ?? []);
        $summary['candidates_scanned'] = (int) ($resolutionPayload['candidates_scanned'] ?? 0);
        $summary['candidates_total'] = (int) ($resolutionPayload['candidates_total'] ?? 0);
        $summary['target_identifiers'] = is_array($resolutionPayload['target_identifiers'] ?? null)
            ? $resolutionPayload['target_identifiers']
            : [];

        if ($resolution->hasReferences()) {
            $summary = array_merge(
                $summary,
                $this->invalidatePreciseMatches($cacheAdapter, $adapter, $normalizedFormId, $resolution, $context)
            );
            $this->logSummary($summary);
            return;
        }

        $useFallback = (bool) ($global['forms_global_fallback_enabled'] ?? true);
        /**
         * Filter whether forms uncertainty should trigger fallback invalidation.
         *
         * @param bool                 $useFallback Default fallback state.
         * @param FormAdapterInterface $adapter Form adapter.
         * @param string               $formId Form ID.
         * @param array<string,mixed>  $resolution Resolver payload.
         */
        $useFallback = (bool) apply_filters(
            'ekesto_ci_forms_global_fallback',
            $useFallback,
            $adapter,
            $normalizedFormId,
            $resolutionPayload
        );
        $summary['fallback_enabled'] = $useFallback;

        if ($resolution->uncertain() && $useFallback) {
            $summary = array_merge(
                $summary,
                $this->invalidateFallbackTargets($cacheAdapter, $adapter, $normalizedFormId, $resolution, $context)
            );
            $this->logSummary($summary);
            return;
        }

        if ($resolution->uncertain()) {
            $summary['outcome'] = 'fallback_disabled';
            $this->logSummary($summary);
            return;
        }

        $summary['outcome'] = 'no_references_no_uncertainty';
        $this->logSummary($summary);
    }

    /**
     * Purge precise post references for a changed form.
     *
     * @param \Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterInterface $cacheAdapter Cache adapter.
     * @param FormAdapterInterface                                        $adapter Form adapter.
     * @param string                                                      $formId Form ID.
     * @param FormsResolutionResult                                       $resolution Resolver result.
     * @param array<string,mixed>                                         $context Event context.
     * @return array<string,mixed> Summary payload for the event log line.
     */
    private function invalidatePreciseMatches(
        \Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterInterface $cacheAdapter,
        FormAdapterInterface $adapter,
        string $formId,
        FormsResolutionResult $resolution,
        array $context
    ): array {
        $postIds = $resolution->postIds();
        if (empty($postIds)) {
            return [
                'outcome' => 'precise_skipped_no_post_ids',
                'target_ids' => 0,
                'groups' => 0,
                'emitted_urls' => 0,
                'purged_urls_directly' => 0,
                'purge_skipped_urls' => 0,
                'intent' => 'presentation',
                'resolution' => 'global_targeted',
                'fallback' => false,
                'fallback_reason' => '',
            ];
        }

        $resolver = LanguageResolverFactory::create();
        $result = PurgeCoordinator::purgeTargets(
            $postIds,
            [],
            $cacheAdapter,
            $resolver,
            null,
            [
                'trigger_class' => 'form',
                'trigger_kind' => 'form',
                'trigger_event' => 'form_change',
                'reason' => 'form_change',
                'trigger_post_id' => 0,
                'trigger_post_type' => '',
            ]
        );
        $emittedUrls = $result['emitted_urls'] ?? [];
        $emittedCount = count($emittedUrls);
        $purgedDirectCount = count($result['purged_urls_directly'] ?? []);
        $purgeSkippedCount = count($result['purge_skipped_urls'] ?? []);

        if (empty($emittedUrls)) {
            return [
                'outcome' => 'precise_noop',
                'target_ids' => count($postIds),
                'groups' => 0,
                'emitted_urls' => $emittedCount,
                'purged_urls_directly' => $purgedDirectCount,
                'purge_skipped_urls' => $purgeSkippedCount,
                'intent' => 'presentation',
                'resolution' => 'global_targeted',
                'fallback' => false,
                'fallback_reason' => '',
            ];
        }

        $payload = $resolution->toArray();
        $eventContext = [
            'source' => $cacheAdapter->getKey(),
            'source_label' => $cacheAdapter->getLabel(),
            'reason' => 'form_change',
            'trigger_class' => 'form',
            'trigger_event' => 'form_change',
            'trigger_post_id' => 0,
            'trigger_post_type' => '',
            'groups' => [],
            'form_adapter' => $adapter->key(),
            'form_id' => $formId,
            'pattern_references' => [],
            'form_references' => $postIds,
            'forms_resolution' => $payload,
            'purged_urls_directly' => $result['purged_urls_directly'] ?? [],
            'purge_skipped_urls' => $result['purge_skipped_urls'] ?? [],
            'fanout_mode' => $result['fanout_mode'] ?? 'same_language',
            'fanout_languages' => $result['fanout_languages'] ?? [],
            'intent' => 'presentation',
            'resolution' => 'global_targeted',
            'fallback' => false,
            'fallback_reason' => '',
            'synchronous_targeted_resolution' => true,
            'trusted_urls' => TrustDecider::trustedUrlsFromOriginUrls([
                TrustDecider::ORIGIN_STRUCTURAL_TARGETS => $emittedUrls,
            ]),
        ];
        Debug::verbose('[ECI] untrusted origin', [
            'origin' => 'derived',
            'reason' => 'forms_precise',
        ]);

        foreach ($context as $key => $value) {
            $eventContext[(string) $key] = $value;
        }

        $emissionPayload = EmissionBuilder::build((array) ($eventContext['trusted_urls'] ?? []), $emittedUrls);
        $eventContext['trusted_urls'] = $emissionPayload['trusted_urls'];
        EmissionHelper::emit($emissionPayload['urls'], $eventContext);

        return [
            'outcome' => 'precise_emitted',
            'target_ids' => count($postIds),
            'groups' => 0,
            'emitted_urls' => $emittedCount,
            'purged_urls_directly' => $purgedDirectCount,
            'purge_skipped_urls' => $purgeSkippedCount,
            'intent' => 'presentation',
            'resolution' => 'global_targeted',
            'fallback' => false,
            'fallback_reason' => '',
        ];
    }

    /**
     * Purge fallback target pages when forms resolution is uncertain.
     *
     * @param \Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterInterface $cacheAdapter Cache adapter.
     * @param FormAdapterInterface                                        $adapter Form adapter.
     * @param string                                                      $formId Form ID.
     * @param FormsResolutionResult                                       $resolution Resolver result.
     * @param array<string,mixed>                                         $context Event context.
     * @return array<string,mixed> Summary payload for the event log line.
     */
    private function invalidateFallbackTargets(
        \Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterInterface $cacheAdapter,
        FormAdapterInterface $adapter,
        string $formId,
        FormsResolutionResult $resolution,
        array $context
    ): array {
        $contextPayload = [
            'trigger_class' => 'form',
            'trigger_kind' => 'form',
            'trigger_event' => 'form_change_fallback',
            'reason' => 'form_change_fallback',
            'trigger_post_id' => 0,
            'trigger_post_type' => '',
            'form_adapter' => $adapter->key(),
            'form_id' => $formId,
            'forms_resolution' => $resolution->toArray(),
            'form_fallback_used' => true,
            'intent' => 'presentation',
            'resolution' => 'global_invalidation',
            'fallback' => true,
            'fallback_reason' => 'resolver_uncertain',
            'trusted_urls' => TrustDecider::trustedUrlsFromOriginUrls([
                TrustDecider::ORIGIN_STRUCTURAL_TARGETS => [],
            ]),
        ];
        Debug::verbose('[ECI] untrusted origin', [
            'origin' => 'derived',
            'reason' => 'forms_fallback',
        ]);

        foreach ($context as $key => $value) {
            $contextPayload[(string) $key] = $value;
        }

        $resolverResult = [
            'post_ids' => $resolution->postIds(),
            'urls' => [],
            'certain' => !$resolution->uncertain(),
        ];
        $resolver = LanguageResolverFactory::create();
        $result = GlobalInvalidationExecutor::executeGlobalTargetedOrEscalate(
            $cacheAdapter,
            $resolver,
            null,
            $resolverResult,
            'form_change_fallback',
            $contextPayload
        );

        $emittedUrls = is_array($result['emitted_urls'] ?? null) ? $result['emitted_urls'] : [];
        $emittedCount = count($emittedUrls);
        $purgedDirectCount = count((array) ($result['purged_urls_directly'] ?? []));
        $purgeSkippedCount = count((array) ($result['purge_skipped_urls'] ?? []));
        $targetIdsCount = count((array) (($result['targets']['post_ids'] ?? [])));
        $outcome = $emittedCount > 0 ? 'fallback_emitted' : 'fallback_noop';
        $outcomeContext = is_array($result['context'] ?? null) ? $result['context'] : [];
        $resolvedIntent = (string) ($outcomeContext['intent'] ?? 'presentation');
        $resolvedResolution = (string) ($outcomeContext['resolution'] ?? 'global_invalidation');
        $resolvedFallback = (bool) ($outcomeContext['fallback'] ?? true);
        $resolvedFallbackReason = (string) ($outcomeContext['fallback_reason'] ?? 'resolver_uncertain');

        return [
            'outcome' => $outcome,
            'target_ids' => $targetIdsCount,
            'groups' => 0,
            'emitted_urls' => $emittedCount,
            'purged_urls_directly' => $purgedDirectCount,
            'purge_skipped_urls' => $purgeSkippedCount,
            'intent' => $resolvedIntent,
            'resolution' => $resolvedResolution,
            'fallback' => $resolvedFallback,
            'fallback_reason' => $resolvedFallbackReason,
        ];
    }

    /**
     * Write one compact summary line for each form-change event.
     *
     * @param array<string,mixed> $summary Event summary fields.
     * @return void
     */
    private function logSummary(array $summary): void
    {
        Debug::log(sprintf(
            'forms_change summary: seq=%d adapter=%s form_id=%s hook=%s event=%s outcome=%s refs=%d uncertain=%s fallback=%s emitted_urls=%d intent=%s resolution=%s fallback_reason=%s',
            (int) ($summary['event_sequence'] ?? 0),
            (string) ($summary['adapter'] ?? ''),
            (string) ($summary['form_id'] ?? ''),
            (string) ($summary['source_hook'] ?? 'unknown'),
            (string) ($summary['event'] ?? 'unknown'),
            (string) ($summary['outcome'] ?? 'unknown'),
            (int) ($summary['references'] ?? 0),
            $this->boolLabel((bool) ($summary['uncertain'] ?? false)),
            $this->boolLabel((bool) ($summary['fallback_enabled'] ?? false)),
            (int) ($summary['emitted_urls'] ?? 0),
            (string) ($summary['intent'] ?? 'presentation'),
            (string) ($summary['resolution'] ?? ''),
            (string) ($summary['fallback_reason'] ?? '')
        ), [
            'event_key' => (string) ($summary['event_key'] ?? ''),
            'target_ids' => (int) ($summary['target_ids'] ?? 0),
            'groups' => (int) ($summary['groups'] ?? 0),
            'target_identifiers_count' => (int) ($summary['target_identifiers_count'] ?? 0),
            'target_identifiers' => $summary['target_identifiers'] ?? [],
            'candidates_scanned' => (int) ($summary['candidates_scanned'] ?? 0),
            'candidates_total' => (int) ($summary['candidates_total'] ?? 0),
            'uncertain_scan_gap' => (bool) ($summary['uncertain_scan_gap'] ?? false),
            'uncertain_meta_too_large' => (bool) ($summary['uncertain_meta_too_large'] ?? false),
            'purged_urls_directly' => (int) ($summary['purged_urls_directly'] ?? 0),
            'purge_skipped_urls' => (int) ($summary['purge_skipped_urls'] ?? 0),
            'intent' => (string) ($summary['intent'] ?? 'presentation'),
            'resolution' => (string) ($summary['resolution'] ?? ''),
            'fallback' => (bool) ($summary['fallback'] ?? false),
            'fallback_reason' => (string) ($summary['fallback_reason'] ?? ''),
        ]);
    }

    /**
     * Convert booleans to yes/no labels for compact log messages.
     *
     * @param bool $value Input value.
     * @return string
     */
    private function boolLabel(bool $value): string
    {
        return $value ? 'yes' : 'no';
    }
}
