<?php

namespace App\Campaigns;

use App\Models\Campaign;
use App\Models\CampaignPerformanceHourStat;
use App\Models\CampaignPerformanceProfile;
use App\Models\CampaignPerformanceStat;
use App\Models\CampaignPerformanceSuggestion;
use App\Models\CampaignPerformanceVerdictLog;
use App\Models\GoogleAdsCampaign;
use App\Services\AI\CampaignPerformanceAdvisor;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Throwable;

class CampaignPerformanceMapper
{
    public function __construct(
        private readonly CampaignPerformanceAdvisor $advisor,
        private readonly CampaignPerformanceVerdictEvaluator $verdicts,
        private readonly CampaignPerformanceWhaleEnrichment $whale,
        private readonly PerformanceWhaleVerdictOverlay $whaleVerdict,
    ) {}

    /**
     * Map dumped stats into the latest suggestion per campaign. Never mutates platforms.
     *
     * @param  'hourly'|'daily'|'manual'  $runType
     * @return array{attempted: int, mapped: int, failed: int}
     */
    public function run(
        ?string $platform = null,
        ?string $campaign = null,
        ?int $limit = null,
        string $runType = 'daily',
    ): array {
        $attempted = 0;
        $mapped = 0;
        $failed = 0;

        foreach ($this->groups($platform, $campaign, $limit) as $group) {
            $attempted++;

            try {
                $this->mapGroup($group, $runType);
                $mapped++;
            } catch (Throwable $exception) {
                $failed++;
                Log::warning('Campaign performance mapping failed', [
                    'platform' => $group['platform'],
                    'external_campaign_id' => $group['external_campaign_id'],
                    'error' => $exception->getMessage(),
                ]);
            }
        }

        return compact('attempted', 'mapped', 'failed');
    }

    /**
     * @return list<array{
     *     platform: string,
     *     external_campaign_id: string,
     *     user_id: int,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int,
     *     days: Collection<int, CampaignPerformanceStat>,
     *     hours: Collection<int, CampaignPerformanceHourStat>
     * }>
     */
    private function groups(?string $platform, ?string $campaign, ?int $limit): array
    {
        $daysQuery = CampaignPerformanceStat::query()
            ->when(is_string($platform) && $platform !== '', fn ($builder) => $builder->where('platform', strtolower($platform)))
            ->when(is_string($campaign) && $campaign !== '', fn ($builder) => $builder->where('external_campaign_id', $campaign))
            ->orderBy('stat_date');

        $daysByCampaign = $daysQuery->get()->groupBy(
            fn (CampaignPerformanceStat $row): string => $row->platform.'|'.$row->external_campaign_id,
        );

        $hoursByCampaign = $this->recentHours($platform, $campaign)->groupBy(
            fn (CampaignPerformanceHourStat $row): string => $row->platform.'|'.$row->external_campaign_id,
        );

        $keys = $daysByCampaign->keys()->merge($hoursByCampaign->keys())->unique()->values();
        $rows = [];

        foreach ($keys as $key) {
            /** @var Collection<int, CampaignPerformanceStat> $days */
            $days = ($daysByCampaign->get($key) ?? collect())->values();
            /** @var Collection<int, CampaignPerformanceHourStat> $hours */
            $hours = ($hoursByCampaign->get($key) ?? collect())->values();
            $first = $days->first() ?? $hours->first();

            if ($first === null) {
                continue;
            }

            $rows[] = [
                'platform' => (string) $first->platform,
                'external_campaign_id' => (string) $first->external_campaign_id,
                'user_id' => (int) $first->user_id,
                'local_campaign_id' => $first->local_campaign_id,
                'local_google_ads_campaign_id' => $first->local_google_ads_campaign_id,
                'days' => $days,
                'hours' => $hours,
            ];
        }

        if ($limit !== null) {
            return array_slice($rows, 0, max(0, $limit));
        }

        $cap = (int) config('campaign-performance.limit', 5);

        return array_slice($rows, 0, max(0, $cap));
    }

    /**
     * @param  array{
     *     platform: string,
     *     external_campaign_id: string,
     *     user_id: int,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int,
     *     days: Collection<int, CampaignPerformanceStat>,
     *     hours: Collection<int, CampaignPerformanceHourStat>
     * }  $group
     * @param  'hourly'|'daily'|'manual'  $runType
     */
    private function mapGroup(array $group, string $runType = 'daily'): void
    {
        $snapshot = $this->snapshot($group);
        $snapshot = $this->whale->enrichSnapshot($snapshot, $group['external_campaign_id']);
        $evaluation = $this->verdicts->evaluate($snapshot, $group['days'], $group['hours'], $runType);

        [$finalVerdict, $finalReason] = $this->whaleVerdict->apply(
            $snapshot,
            $evaluation['final_verdict'],
            $evaluation['final_reason'],
        );
        $evaluation['final_verdict'] = $finalVerdict;
        $evaluation['final_reason'] = $finalReason;
        $snapshot['verdicts'] = [
            'run_type' => $runType,
            'windows' => $evaluation['windows'],
            'hourly_verdict' => $evaluation['hourly_verdict'],
            'final_verdict' => $evaluation['final_verdict']->value,
            'final_reason' => $evaluation['final_reason'],
        ];
        $snapshot['allowed_verdicts'] = array_map(
            fn (PerformanceVerdict $verdict): string => $verdict->value,
            PerformanceVerdict::cases(),
        );

        $this->appendVerdictLog($group, $snapshot, $evaluation, $runType);

        $snapshot['final_verdict'] = $evaluation['final_verdict']->value;
        $snapshot['final_reason'] = $evaluation['final_reason'];

        $php = new PerformanceAdvice(
            status: $evaluation['final_verdict'],
            reason: $evaluation['final_reason'],
            source: 'php',
        );
        $advice = $this->advisor->explain($snapshot, $php);

        $row = CampaignPerformanceSuggestion::query()
            ->where('platform', $group['platform'])
            ->where('external_campaign_id', $group['external_campaign_id'])
            ->first() ?? new CampaignPerformanceSuggestion;

        $row->fill([
            'user_id' => $group['user_id'],
            'platform' => $group['platform'],
            'external_campaign_id' => $group['external_campaign_id'],
            'local_campaign_id' => $group['local_campaign_id'],
            'local_google_ads_campaign_id' => $group['local_google_ads_campaign_id'],
            'status' => $advice->status->value,
            'source' => $advice->source,
            'reason' => $advice->reason,
            'confidence' => $advice->confidence,
            'action_state' => 'pending',
            'snapshot' => $snapshot,
            'model' => $advice->model,
            'last_error' => $advice->lastError,
            'suggested_at' => now(),
        ]);
        $row->save();
    }

    /**
     * @param  array{
     *     platform: string,
     *     external_campaign_id: string,
     *     user_id: int,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int
     * }  $group
     * @param  array<string, mixed>  $snapshot
     * @param  array{
     *     windows: array<string, array<string, mixed>>,
     *     hourly_verdict: ?array{verdict: string, reason: string},
     *     final_verdict: PerformanceVerdict,
     *     final_reason: string
     * }  $evaluation
     * @param  'hourly'|'daily'|'manual'  $runType
     */
    private function appendVerdictLog(array $group, array $snapshot, array $evaluation, string $runType): void
    {
        CampaignPerformanceVerdictLog::query()->create([
            'user_id' => $group['user_id'],
            'platform' => $group['platform'],
            'external_campaign_id' => $group['external_campaign_id'],
            'local_campaign_id' => $group['local_campaign_id'],
            'local_google_ads_campaign_id' => $group['local_google_ads_campaign_id'],
            'evaluated_at' => now(),
            'run_type' => $runType,
            'final_verdict' => $evaluation['final_verdict']->value,
            'final_reason' => $evaluation['final_reason'],
            'platform_status' => $snapshot['platform_status'] ?? null,
            'campaign' => is_array($snapshot['campaign_profile'] ?? null)
                ? $snapshot['campaign_profile']
                : [
                    'name' => $snapshot['campaign'] ?? null,
                    'platform' => $group['platform'],
                    'external_campaign_id' => $group['external_campaign_id'],
                ],
            'windows' => $evaluation['windows'],
            'hourly_verdict' => $evaluation['hourly_verdict'],
            'source' => 'php',
        ]);
    }

    /**
     * @param  array{
     *     platform: string,
     *     external_campaign_id: string,
     *     user_id: int,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int,
     *     days: Collection<int, CampaignPerformanceStat>,
     *     hours: Collection<int, CampaignPerformanceHourStat>
     * }  $group
     * @return array<string, mixed>
     */
    private function snapshot(array $group): array
    {
        $since = now()->subDays(6)->startOfDay();
        $window = $group['days']->filter(
            fn (CampaignPerformanceStat $row): bool => $row->stat_date !== null && $row->stat_date->gte($since),
        );
        $yesterday = $group['days']->first(
            fn (CampaignPerformanceStat $row): bool => $row->stat_date?->toDateString() === now()->subDay()->toDateString(),
        );
        $latestDay = $group['days']->sortByDesc(
            fn (CampaignPerformanceStat $row): int => $row->stat_date?->timestamp ?? 0,
        )->first();
        $latestHour = $group['hours']->sortByDesc(
            fn (CampaignPerformanceHourStat $row): int => $row->stat_hour?->timestamp ?? 0,
        )->first();
        $latest = $latestHour ?? $latestDay;

        $spend = round((float) $window->sum('spend'), 2);
        $impressions = (int) $window->sum('impressions');
        $clicks = (int) $window->sum('clicks');
        $firstDate = $group['days']->min('stat_date') ?? $group['hours']->min('stat_hour');
        $daysLive = $this->daysLive($firstDate, $group);
        $hourTotals = $this->hourTotals($group['hours']);

        $syncFailed = $this->syncFailed($window, $group['days'], $group['hours'], $spend, $impressions);

        $profile = CampaignPerformanceProfile::query()
            ->where('platform', $group['platform'])
            ->where('external_campaign_id', $group['external_campaign_id'])
            ->first();
        [$name, $budget] = $this->campaignMeta($group, $profile);
        $metrics = PerformanceTrend::fromDays($group['days']);

        return [
            'campaign' => $name,
            'platform' => $group['platform'],
            'platform_status' => $profile?->platform_status ?? $latest?->platform_status,
            'delivery_status' => $profile?->delivery_status ?? $latest?->delivery_status,
            'daily_budget' => $budget,
            'campaign_profile' => $profile?->toSnapshotArray(),
            'days_live' => $daysLive,
            'learning_window_days' => (int) config('campaign-performance.learning_window_days', 3),
            'max_test_spend' => (float) config('campaign-performance.max_test_spend', 40),
            'last_7_days' => $metrics['last_7_days'],
            'previous_7_days' => $metrics['previous_7_days'],
            'last_7_hours' => $hourTotals,
            'lifetime' => $metrics['lifetime'],
            'trend' => $metrics['trend'],
            'trend_summary' => $metrics['trend_summary'],
            'trend_cpc_change_pct' => $metrics['trend_cpc_change_pct'],
            'trend_clicks_change_pct' => $metrics['trend_clicks_change_pct'],
            'yesterday' => [
                'spend' => round((float) ($yesterday?->spend ?? 0), 2),
                'impressions' => (int) ($yesterday?->impressions ?? 0),
                'clicks' => (int) ($yesterday?->clicks ?? 0),
            ],
            'rules' => [
                'zero_click_spend_floor' => (float) config('campaign-performance.zero_click_spend_floor', 10),
                'max_cpc' => (float) config('campaign-performance.max_cpc', 2),
            ],
            'sync_failed' => $syncFailed,
            'sync_error' => $latest?->last_error,
        ];
    }

    /**
     * @return Collection<int, CampaignPerformanceHourStat>
     */
    private function recentHours(?string $platform, ?string $campaign): Collection
    {
        $hours = PerformanceWindows::maxAnalysisHours();
        $since = now()->copy()->startOfHour()->subHours($hours - 1);

        return CampaignPerformanceHourStat::query()
            ->when(is_string($platform) && $platform !== '', fn ($builder) => $builder->where('platform', strtolower($platform)))
            ->when(is_string($campaign) && $campaign !== '', fn ($builder) => $builder->where('external_campaign_id', $campaign))
            ->where('stat_hour', '>=', $since)
            ->orderBy('stat_hour')
            ->get();
    }

    /**
     * @param  Collection<int, CampaignPerformanceHourStat>  $hours
     * @return array{spend: float, impressions: int, clicks: int, cpc: ?float, ctr: ?float, hours: int}
     */
    private function hourTotals(Collection $hours): array
    {
        $spend = round((float) $hours->sum('spend'), 2);
        $impressions = (int) $hours->sum('impressions');
        $clicks = (int) $hours->sum('clicks');

        return [
            'spend' => $spend,
            'impressions' => $impressions,
            'clicks' => $clicks,
            'cpc' => $clicks > 0 ? round($spend / $clicks, 4) : null,
            'ctr' => $impressions > 0 ? round($clicks / $impressions, 4) : null,
            'hours' => $hours->count(),
        ];
    }

    /**
     * @param  Collection<int, CampaignPerformanceStat>  $window
     * @param  Collection<int, CampaignPerformanceStat>  $days
     * @param  Collection<int, CampaignPerformanceHourStat>  $hours
     */
    private function syncFailed(Collection $window, Collection $days, Collection $hours, float $spend, int $impressions): bool
    {
        if ($window->isEmpty()) {
            if ($days->contains(fn (CampaignPerformanceStat $row): bool => filled($row->last_error))) {
                return true;
            }

            return $hours->isNotEmpty()
                && $hours->every(fn (CampaignPerformanceHourStat $row): bool => filled($row->last_error))
                && $spend < 0.01
                && $impressions === 0;
        }

        return $window->every(fn (CampaignPerformanceStat $row): bool => filled($row->last_error))
            && $spend < 0.01
            && $impressions === 0;
    }

    /**
     * @param  array{
     *     platform: string,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int,
     *     days: Collection<int, CampaignPerformanceStat>
     * }  $group
     */
    private function daysLive(mixed $firstDate, array $group): int
    {
        $start = $firstDate instanceof Carbon
            ? $firstDate
            : (is_string($firstDate) && $firstDate !== '' ? Carbon::parse($firstDate) : null);

        if ($start === null && $group['platform'] === 'meta' && $group['local_campaign_id']) {
            $start = Campaign::query()->find($group['local_campaign_id'])?->created_at;
        }

        if ($start === null && $group['platform'] === 'google' && $group['local_google_ads_campaign_id']) {
            $start = GoogleAdsCampaign::query()->find($group['local_google_ads_campaign_id'])?->created_at;
        }

        if ($start === null) {
            return 0;
        }

        return (int) $start->startOfDay()->diffInDays(now()->startOfDay()) + 1;
    }

    /**
     * @param  array{
     *     platform: string,
     *     external_campaign_id: string,
     *     local_campaign_id: ?int,
     *     local_google_ads_campaign_id: ?int
     * }  $group
     * @return array{0: string, 1: ?float}
     */
    private function campaignMeta(array $group, ?CampaignPerformanceProfile $profile = null): array
    {
        if ($group['platform'] === 'google') {
            $campaign = $group['local_google_ads_campaign_id']
                ? GoogleAdsCampaign::query()->find($group['local_google_ads_campaign_id'])
                : null;

            return [
                filled($profile?->name)
                    ? (string) $profile->name
                    : ($campaign?->name ?: 'Campaign '.$group['external_campaign_id']),
                $profile?->daily_budget !== null
                    ? (float) $profile->daily_budget
                    : ($campaign?->daily_budget !== null ? (float) $campaign->daily_budget : null),
            ];
        }

        $campaign = $group['local_campaign_id']
            ? Campaign::query()->find($group['local_campaign_id'])
            : Campaign::query()->where('external_campaign_id', $group['external_campaign_id'])->first();

        return [
            filled($profile?->name)
                ? (string) $profile->name
                : ($campaign?->name ?: 'Campaign '.$group['external_campaign_id']),
            $profile?->daily_budget !== null
                ? (float) $profile->daily_budget
                : ($campaign?->budget !== null ? (float) $campaign->budget : null),
        ];
    }
}
