<?php

namespace App\Library\CampaignDecision;

class CampaignUnifiedVerdictCombiner
{
    /**
     * @param  array<string, mixed>  $channels
     * @param  array<string, float|int>  $settings
     * @return array{
     *     verdict: CampaignDecisionVerdict,
     *     reason: string,
     *     reasons: list<array<string, mixed>>,
     *     recommended_action: string,
     *     recommended_budget_change_percent: int
     * }
     */
    public function combine(
        CampaignPerformanceContext $context,
        array $channels,
        array $settings,
        array $dataQuality,
    ): array {
        $reasons = [];

        if (($dataQuality['status'] ?? '') === 'BROKEN') {
            foreach ($dataQuality['reasons'] as $row) {
                $reasons[] = [...$row, 'channel' => 'platform'];
            }

            return $this->result(
                CampaignDecisionVerdict::DataIssue,
                'Fix platform ingest or mapping before changing the campaign.',
                $reasons,
                'NONE',
                0,
            );
        }

        if ($this->isLearning($context, $settings)) {
            $reasons[] = [
                'metric' => 'learning_period',
                'channel' => 'platform',
                'impact' => 'neutral',
                'current' => $context->campaignAgeDays,
                'target' => (int) ($settings['learning_period_days'] ?? 3),
            ];

            if ($this->hourCriticalPause($channels, $settings)) {
                $reasons[] = [
                    'metric' => 'hour_spend_no_clicks',
                    'channel' => 'hour',
                    'impact' => 'negative',
                ];

                return $this->result(
                    CampaignDecisionVerdict::Pause,
                    'Critical hour-window spend with no clicks during learning.',
                    $reasons,
                    'PAUSE',
                    0,
                );
            }

            return $this->result(
                CampaignDecisionVerdict::Learning,
                'Campaign is still in the learning period — monitoring only.',
                $reasons,
                'NONE',
                0,
            );
        }

        if ($this->hourCriticalPause($channels, $settings)) {
            $reasons[] = [
                'metric' => 'hour_spend_or_cpc',
                'channel' => 'hour',
                'impact' => 'negative',
            ];

            return $this->result(
                CampaignDecisionVerdict::Pause,
                'Recent hours show wasteful spend (no clicks or CPC above limit).',
                $reasons,
                'PAUSE',
                0,
            );
        }

        $outcome = $channels['outcome'] ?? null;

        if (is_array($outcome) && ($outcome['available'] ?? false)) {
            $outcomeVerdict = $this->evaluateOutcome($outcome, $settings, $reasons);

            if ($outcomeVerdict !== null) {
                return $outcomeVerdict;
            }
        }

        if (! $this->hasEvaluationVolume($channels, $settings)) {
            return $this->result(
                CampaignDecisionVerdict::InsufficientData,
                'Not enough spend or clicks yet for a revenue-based decision.',
                $reasons,
                'NONE',
                0,
            );
        }

        if ($this->hourChannelAlarming($channels, $settings)) {
            $reasons[] = ['metric' => 'hour_channel', 'channel' => 'hour', 'impact' => 'negative'];

            return $this->result(
                CampaignDecisionVerdict::Watch,
                'Platform hour metrics are weak; wait for lagged outcome data to confirm.',
                $reasons,
                'NONE',
                0,
            );
        }

        return $this->result(
            CampaignDecisionVerdict::Maintain,
            'Metrics are within acceptable limits on available windows.',
            $reasons,
            'NO_CHANGE',
            0,
        );
    }

    /**
     * @param  array<string, float|int>  $settings
     */
    private function isLearning(CampaignPerformanceContext $context, array $settings): bool
    {
        $learningDays = (int) ($settings['learning_period_days'] ?? 3);
        $maxTestSpend = (float) ($settings['max_test_spend'] ?? 40);
        $lifetimeSpend = (float) ($context->windows['7d']['spend'] ?? 0);

        if (($context->affiliate['revenue_source'] ?? null) === 'roi_dashboard') {
            $lifetimeSpend = max($lifetimeSpend, (float) data_get($context->affiliate, 'outcome_window.spend', 0));
        }

        if ($context->campaignAgeDays < $learningDays) {
            return true;
        }

        if ($lifetimeSpend < $maxTestSpend) {
            return true;
        }

        $delivery = strtoupper((string) ($context->deliveryStatus ?? ''));

        return $delivery === 'LEARNING';
    }

    /**
     * @param  array<string, mixed>  $channels
     * @param  array<string, float|int>  $settings
     */
    private function hourCriticalPause(array $channels, array $settings): bool
    {
        $hour = $channels['hour'] ?? [];
        $floor = (float) ($settings['zero_click_spend_floor'] ?? 10);
        $maxCpc = (float) ($settings['max_cpc'] ?? 2);
        $spend = (float) ($hour['spend'] ?? 0);
        $clicks = (int) ($hour['clicks'] ?? 0);
        $cpc = $hour['cpc'] ?? null;

        if ($spend >= $floor && $clicks === 0) {
            return true;
        }

        return $clicks > 0 && $cpc !== null && $cpc > $maxCpc && $spend >= $floor;
    }

    /**
     * @param  array<string, mixed>  $channels
     * @param  array<string, float|int>  $settings
     */
    private function hourChannelAlarming(array $channels, array $settings): bool
    {
        $hour = $channels['hour'] ?? [];
        $maxCpc = (float) ($settings['max_cpc'] ?? 2);

        return isset($hour['cpc']) && $hour['cpc'] > $maxCpc * 0.9;
    }

    /**
     * @param  array<string, mixed>  $channels
     * @param  array<string, float|int>  $settings
     */
    private function hasEvaluationVolume(array $channels, array $settings): bool
    {
        $day = $channels['day'] ?? [];
        $minSpend = (float) ($settings['minimum_spend'] ?? 25);
        $minClicks = (int) ($settings['minimum_clicks'] ?? 20);

        return (float) ($day['spend'] ?? 0) >= $minSpend
            || (int) ($day['clicks'] ?? 0) >= $minClicks;
    }

    /**
     * @param  array<string, mixed>  $outcome
     * @param  array<string, float|int>  $settings
     * @param  list<array<string, mixed>>  $reasons
     * @return array{
     *     verdict: CampaignDecisionVerdict,
     *     reason: string,
     *     reasons: list<array<string, mixed>>,
     *     recommended_action: string,
     *     recommended_budget_change_percent: int
     * }|null
     */
    private function evaluateOutcome(array $outcome, array $settings, array &$reasons): ?array
    {
        $roas = $outcome['roas'] ?? null;
        $cpa = $outcome['cpa'] ?? null;
        $orders = (float) ($outcome['orders'] ?? 0);
        $revenue = (float) ($outcome['revenue'] ?? 0);
        $spend = (float) ($outcome['platform_spend'] ?? 0);
        $roi = ($outcome['revenue_source'] ?? null) === 'roi_dashboard';
        $minRoas = (float) ($settings['minimum_roas'] ?? 1.2);
        $targetRoas = (float) ($settings['target_roas'] ?? 2.0);
        $maxCpa = (float) ($settings['maximum_cpa'] ?? 75);
        $targetCpa = (float) ($settings['target_cpa'] ?? 50);
        $minOrders = (float) ($settings['minimum_orders'] ?? 1);
        $minSpend = (float) ($settings['minimum_spend'] ?? 25);

        if ($roi && $revenue > 0) {
            $orders = max($orders, $minOrders);
        }

        if ($roi && $revenue <= 0 && $spend >= $minSpend) {
            $countVerdict = $this->evaluateResultCount($outcome, $spend, $settings, $reasons);

            if ($countVerdict !== null) {
                return $countVerdict;
            }
        }

        if ($spend >= $minSpend && $orders < $minOrders) {
            $reasons[] = [
                'metric' => 'orders',
                'channel' => 'outcome',
                'current' => $orders,
                'target' => $minOrders,
                'impact' => 'negative',
            ];

            return $this->result(
                CampaignDecisionVerdict::Pause,
                $roi
                    ? 'ROI spend in the lagged window has no sales.'
                    : 'Mature window has spend but no affiliate orders.',
                $reasons,
                'PAUSE',
                0,
            );
        }

        if ($roas !== null && $roas >= $targetRoas && $orders >= $minOrders) {
            $pct = (int) ($settings['scale_up_percentage'] ?? 10);
            $reasons[] = [
                'metric' => 'ROAS',
                'channel' => 'outcome',
                'current' => $roas,
                'target' => $targetRoas,
                'impact' => 'positive',
            ];

            return $this->result(
                CampaignDecisionVerdict::ScaleUp,
                sprintf('Lagged %dd ROAS %.2f meets target.', (int) config('campaign-decisions.monitor.primary_outcome_window_days', 7), $roas),
                $reasons,
                'SCALE_UP',
                $pct,
            );
        }

        if ($roas !== null && $roas < $minRoas && $orders >= $minOrders) {
            $pct = -(int) ($settings['decrease_budget_percentage'] ?? 10);
            $reasons[] = [
                'metric' => 'ROAS',
                'channel' => 'outcome',
                'current' => $roas,
                'target' => $minRoas,
                'impact' => 'negative',
            ];

            return $this->result(
                CampaignDecisionVerdict::DecreaseBudget,
                'ROAS is below minimum but orders are still coming in.',
                $reasons,
                'DECREASE_BUDGET',
                $pct,
            );
        }

        if ($cpa !== null && $cpa > $maxCpa && $orders >= $minOrders) {
            $reasons[] = [
                'metric' => 'CPA',
                'channel' => 'outcome',
                'current' => $cpa,
                'maximum' => $maxCpa,
                'impact' => 'negative',
            ];

            return $this->result(
                CampaignDecisionVerdict::DecreaseBudget,
                'CPA is above maximum on the lag-adjusted window.',
                $reasons,
                'DECREASE_BUDGET',
                -(int) ($settings['decrease_budget_percentage'] ?? 10),
            );
        }

        if ($roas !== null && $roas >= $minRoas && ($cpa === null || $cpa <= $targetCpa)) {
            $reasons[] = [
                'metric' => 'ROAS',
                'channel' => 'outcome',
                'current' => $roas,
                'target' => $minRoas,
                'impact' => 'positive',
            ];

            return $this->result(
                CampaignDecisionVerdict::Maintain,
                'Lagged outcome metrics are within acceptable bands.',
                $reasons,
                'NO_CHANGE',
                0,
            );
        }

        return null;
    }

    /**
     * When ROI sales are missing, score the strongest result count against spend.
     * Priority: conversions, subscriptions, installs or bookings, leads, arb_orders.
     *
     * @param  array<string, mixed>  $outcome
     * @param  array<string, float|int>  $settings
     * @param  list<array<string, mixed>>  $reasons
     * @return array{
     *     verdict: CampaignDecisionVerdict,
     *     reason: string,
     *     reasons: list<array<string, mixed>>,
     *     recommended_action: string,
     *     recommended_budget_change_percent: int
     * }|null
     */
    private function evaluateResultCount(array $outcome, float $spend, array $settings, array &$reasons): ?array
    {
        $signal = $this->leadingResultCount($outcome);

        if ($signal === null) {
            return null;
        }

        $goodCost = (float) ($settings['good_cost_per_result'] ?? 2);
        $acceptableCost = (float) ($settings['acceptable_cost_per_result'] ?? 4);
        $cost = round($spend / $signal['count'], 2);
        $metric = $signal['metric'];

        $reasons[] = [
            'metric' => 'cost_per_'.$metric,
            'channel' => 'outcome',
            'current' => $cost,
            'count' => $signal['count'],
            'target' => $goodCost,
            'maximum' => $acceptableCost,
            'impact' => $cost <= $acceptableCost ? 'positive' : 'negative',
        ];

        if ($cost <= $goodCost) {
            return $this->result(
                CampaignDecisionVerdict::Maintain,
                sprintf('No sales. Cost per %s is $%.2f, which is a good sign.', str_replace('_', ' ', $metric), $cost),
                $reasons,
                'NO_CHANGE',
                0,
            );
        }

        if ($cost <= $acceptableCost) {
            return $this->result(
                CampaignDecisionVerdict::Watch,
                sprintf('No sales. Cost per %s is $%.2f, which is still acceptable.', str_replace('_', ' ', $metric), $cost),
                $reasons,
                'NONE',
                0,
            );
        }

        return $this->result(
            CampaignDecisionVerdict::DecreaseBudget,
            sprintf('No sales. Cost per %s is $%.2f, which is above $%.2f.', str_replace('_', ' ', $metric), $cost, $acceptableCost),
            $reasons,
            'DECREASE_BUDGET',
            -(int) ($settings['decrease_budget_percentage'] ?? 10),
        );
    }

    /**
     * @param  array<string, mixed>  $outcome
     * @return array{metric: string, count: float}|null
     */
    private function leadingResultCount(array $outcome): ?array
    {
        $conversions = (float) ($outcome['conversions'] ?? 0);

        if ($conversions > 0) {
            return ['metric' => 'conversions', 'count' => $conversions];
        }

        $subscriptions = (float) ($outcome['subscriptions'] ?? 0);

        if ($subscriptions > 0) {
            return ['metric' => 'subscriptions', 'count' => $subscriptions];
        }

        $installs = (float) ($outcome['installs'] ?? 0);
        $bookings = (float) ($outcome['bookings'] ?? 0);

        if ($installs > 0 || $bookings > 0) {
            if ($bookings > $installs) {
                return ['metric' => 'bookings', 'count' => $bookings];
            }

            return ['metric' => 'installs', 'count' => $installs];
        }

        $leads = (float) ($outcome['leads'] ?? 0);

        if ($leads > 0) {
            return ['metric' => 'leads', 'count' => $leads];
        }

        $arbOrders = (float) ($outcome['arb_orders'] ?? 0);

        if ($arbOrders > 0) {
            return ['metric' => 'arb_orders', 'count' => $arbOrders];
        }

        return null;
    }

    /**
     * @param  list<array<string, mixed>>  $reasons
     * @return array{
     *     verdict: CampaignDecisionVerdict,
     *     reason: string,
     *     reasons: list<array<string, mixed>>,
     *     recommended_action: string,
     *     recommended_budget_change_percent: int
     * }
     */
    private function result(
        CampaignDecisionVerdict $verdict,
        string $reason,
        array $reasons,
        string $action,
        int $budgetPercent,
    ): array {
        return [
            'verdict' => $verdict,
            'reason' => $reason,
            'reasons' => $reasons,
            'recommended_action' => $action,
            'recommended_budget_change_percent' => $budgetPercent,
        ];
    }
}
