<?php

namespace App\Campaigns;

use App\Library\CampaignDecision\CampaignDecisionVerdict;
use App\Models\CampaignPerformanceSuggestion;
use App\Models\CampaignPerformanceVerdictLog;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator as Paginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class PerformanceBoard
{
    public function __construct(private readonly WhaleAdopsReader $adops) {}

    /**
     * @param  array{q?: string, platform?: string, status?: string, per_page?: int}  $filters
     * @return array{
     *     rows: LengthAwarePaginator,
     *     stats: array<string, int>,
     *     totals: array<string, float|int|null>
     * }
     */
    public function forUser(User $user, array $filters): array
    {
        $q = trim((string) ($filters['q'] ?? ''));
        $platform = strtolower(trim((string) ($filters['platform'] ?? '')));

        $rows = $this->rowsFor($user);
        $stats = [
            'all' => $rows->count(),
            'none' => $rows->where('status', null)->count(),
        ];

        foreach (CampaignDecisionVerdict::cases() as $verdict) {
            $stats[$verdict->value] = $rows->where('status', $verdict->value)->count();
        }

        foreach (PerformanceVerdict::cases() as $verdict) {
            if (! array_key_exists($verdict->value, $stats)) {
                $stats[$verdict->value] = $rows->where('status', $verdict->value)->count();
            }
        }

        $totals = [
            'spend_7d' => round((float) $rows->sum('spend_7d'), 2),
            'clicks_7d' => (int) $rows->sum('clicks_7d'),
            'impressions_7d' => (int) $rows->sum('impressions_7d'),
        ];
        $totals['cpc_7d'] = $totals['clicks_7d'] > 0
            ? round($totals['spend_7d'] / $totals['clicks_7d'], 4)
            : null;
        $totals['ctr_7d'] = $totals['impressions_7d'] > 0
            ? round($totals['clicks_7d'] / $totals['impressions_7d'], 4)
            : null;

        $status = trim((string) ($filters['status'] ?? ''));

        if ($q !== '') {
            $rows = $rows->filter(function (array $row) use ($q): bool {
                return str_contains(strtolower($row['name']), strtolower($q))
                    || str_contains((string) $row['external_campaign_id'], $q);
            });
        }

        if (in_array($platform, ['meta', 'google', 'linkedin'], true)) {
            $rows = $rows->where('platform', $platform);
        }

        if ($status === 'none') {
            $rows = $rows->where('status', null);
        } elseif (CampaignDecisionVerdict::tryFrom($status) !== null
            || PerformanceVerdict::tryFromStored($status) !== null) {
            $rows = $rows->where('status', $status);
        }

        $perPage = in_array((int) ($filters['per_page'] ?? 10), [10, 25, 50], true)
            ? (int) $filters['per_page']
            : 10;
        $page = max(1, (int) request()->query('page', 1));
        $rows = $rows->values();

        $paginator = new Paginator(
            $rows->forPage($page, $perPage)->values(),
            $rows->count(),
            $perPage,
            $page,
            ['path' => request()->url(), 'query' => request()->query()],
        );

        return [
            'rows' => $paginator,
            'stats' => $stats,
            'totals' => $totals,
        ];
    }

    /**
     * @param  array{
     *     per_page?: int,
     *     log_run_type?: string,
     *     log_verdict?: string,
     *     log_days?: int,
     *     log_per_page?: int
     * }  $filters
     * @return array<string, mixed>|null
     */
    public function detailFor(User $user, string $platform, string $externalId, array $filters = []): ?array
    {
        $platform = strtolower($platform);
        $row = $this->rowsFor($user)->first(
            fn (array $candidate): bool => $candidate['platform'] === $platform
                && $candidate['external_campaign_id'] === $externalId,
        );

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

        $chartDays = collect();
        $hours = collect();

        if ($this->adops->isAvailable() && $platform === 'meta') {
            $chartDays = $this->adops->dailyStats(
                $platform,
                $externalId,
                now()->subDays(89)->toDateString(),
                now()->toDateString(),
            );
            $oldest = $chartDays->min('stat_date');
            $hourSince = $oldest instanceof Carbon
                ? $oldest->copy()->startOfDay()
                : now()->subDays(7)->startOfHour();
            $hours = $this->adops->hourlyStats($platform, $externalId, $hourSince);
        }

        $perPage = in_array((int) ($filters['per_page'] ?? 10), [10, 25, 50], true)
            ? (int) $filters['per_page']
            : 10;
        $page = max(1, (int) request()->query('page', 1));
        $sortedHistory = $chartDays
            ->sortByDesc(fn (PerformanceDay $day): int => $day->stat_date->timestamp)
            ->values();
        $history = new Paginator(
            $sortedHistory->forPage($page, $perPage)->values(),
            $sortedHistory->count(),
            $perPage,
            $page,
            ['path' => request()->url(), 'query' => request()->query()],
        );
        $hourlyByDate = $hours->groupBy(fn (PerformanceHour $hour): string => $hour->stat_hour->toDateString());
        $latestHour = $hours->sortByDesc(fn (PerformanceHour $hour): int => $hour->stat_hour->timestamp)->first();

        $lifetimeSpend = round((float) $chartDays->sum('spend'), 2);
        $lifetimeClicks = (int) $chartDays->sum('clicks');
        $lifetimeImpressions = (int) $chartDays->sum('impressions');

        $suggestion = $this->restrictToUser(CampaignPerformanceSuggestion::query(), $user)
            ->where('platform', $platform)
            ->where('external_campaign_id', $externalId)
            ->first();

        if ($suggestion === null && $this->adops->isAvailable() && $platform === 'meta'
            && config('campaign-performance.adops_list_all')) {
            $suggestion = CampaignPerformanceSuggestion::query()
                ->where('platform', $platform)
                ->where('external_campaign_id', $externalId)
                ->first();
        }

        $verdictLogs = $this->verdictLogsFor($user, $platform, $externalId, $filters);

        $latestVerdict = $this->restrictToUser(CampaignPerformanceVerdictLog::query(), $user)
            ->where('platform', $platform)
            ->where('external_campaign_id', $externalId)
            ->orderByDesc('evaluated_at')
            ->orderByDesc('id')
            ->first();

        $snapshotProfile = data_get($suggestion?->snapshot, 'campaign_profile');
        $liveProfile = array_filter([
            'name' => $row['name'],
            'platform_status' => $row['platform_status'],
            'delivery_status' => $row['delivery_status'],
            'region_label' => $row['region_label'],
            'platform_learning_label' => $row['platform_learning_label'],
            'daily_budget' => $row['daily_budget'] ?? data_get($suggestion?->snapshot, 'daily_budget'),
            'account_id' => $row['account_id'] ?? null,
            'started_at' => $row['started_at'] ?? null,
        ], fn (mixed $value): bool => $value !== null && $value !== '');
        $profile = PerformanceProfileView::fromSnapshot(array_merge(
            is_array($snapshotProfile) ? $snapshotProfile : [],
            $liveProfile,
        ));

        $suggestionWindows = data_get($suggestion?->snapshot, 'verdicts.windows', []);
        $suggestionWindows = is_array($suggestionWindows) ? $suggestionWindows : [];
        $suggestionHourlyVerdict = data_get($suggestion?->snapshot, 'verdicts.hourly_verdict');
        $suggestionRunType = (string) data_get($suggestion?->snapshot, 'verdicts.run_type', '');

        $affiliate = data_get($suggestion?->snapshot, 'affiliate');
        $affiliate = is_array($affiliate) && ($affiliate['matched'] ?? false) ? $affiliate : null;
        $whale = app(CampaignPerformanceWhaleEnrichment::class);
        if ($affiliate === null && $whale->isEnabled()) {
            $liveAffiliate = data_get($whale->enrichSnapshot([], $externalId), 'affiliate');
            $affiliate = is_array($liveAffiliate) && ($liveAffiliate['matched'] ?? false)
                ? $liveAffiliate
                : null;
        }
        $affiliateDaily = $whale->isEnabled() ? $whale->dailyRows($externalId) : [];
        $affiliateDailyByDate = collect($affiliateDaily)->keyBy('date');

        $platformMetrics7d = PerformanceWindowRollup::fromDays('7d', 7, $chartDays);
        $suggestionWindows = $this->mergePlatformMetricsIntoWindows($suggestionWindows, $chartDays);

        return [
            'row' => $row,
            'history' => $history,
            'hourly_by_date' => $hourlyByDate,
            'chart' => PerformanceChart::fromDays($chartDays),
            'suggestion' => $suggestion,
            'suggestion_windows' => $suggestionWindows,
            'suggestion_hourly_verdict' => is_array($suggestionHourlyVerdict) ? $suggestionHourlyVerdict : null,
            'suggestion_run_type' => $suggestionRunType,
            'latest_hour' => $latestHour,
            'profile' => $profile,
            'verdict_logs' => $verdictLogs,
            'latest_verdict' => $latestVerdict,
            'log_filters' => [
                'run_type' => trim((string) ($filters['log_run_type'] ?? '')),
                'verdict' => trim((string) ($filters['log_verdict'] ?? '')),
                'days' => (int) ($filters['log_days'] ?? 0),
                'per_page' => $verdictLogs->perPage(),
            ],
            'local' => null,
            'lifetime' => [
                'spend' => $lifetimeSpend,
                'clicks' => $lifetimeClicks,
                'impressions' => $lifetimeImpressions,
                'cpc' => $lifetimeClicks > 0 ? round($lifetimeSpend / $lifetimeClicks, 4) : null,
            ],
            'daily_budget' => $profile->daily_budget ?? $row['daily_budget'] ?? data_get($suggestion?->snapshot, 'daily_budget'),
            'local_status' => $profile->platform_status,

            'affiliate' => $affiliate,
            'affiliate_daily' => $affiliateDaily,
            'affiliate_daily_by_date' => $affiliateDailyByDate,
            'platform_metrics_7d' => [
                'spend' => $platformMetrics7d['spend'],
                'clicks' => $platformMetrics7d['clicks'],
                'cpc' => $platformMetrics7d['cpc'],
                'conversions' => $platformMetrics7d['conversions'],
                'conversion_value' => $platformMetrics7d['conversion_value'],
            ],
        ];
    }

    /**
     * @param  array<string, array<string, mixed>>  $windows
     * @param  Collection<int, PerformanceDay>  $days
     * @return array<string, array<string, mixed>>
     */
    private function mergePlatformMetricsIntoWindows(array $windows, Collection $days): array
    {
        foreach (['3d' => 3, '7d' => 7] as $windowKey => $length) {
            if (! isset($windows[$windowKey])) {
                continue;
            }

            $rollup = PerformanceWindowRollup::fromDays($windowKey, $length, $days);
            $windows[$windowKey]['conversions'] = $rollup['conversions'];
            $windows[$windowKey]['conversion_value'] = $rollup['conversion_value'];
        }

        return $windows;
    }

    /**
     * @return Collection<int, array<string, mixed>>
     */
    private function rowsFor(User $user): Collection
    {
        $since = now()->subDays(6)->toDateString();
        $identities = $this->identities($user);
        $whaleTotals = $this->adops->isAvailable()
            ? $this->adops->sevenDayTotals('meta', $since)
            : [];
        $suggestions = $this->restrictToUser(CampaignPerformanceSuggestion::query(), $user)
            ->get()
            ->keyBy(fn (CampaignPerformanceSuggestion $row): string => $row->platform.'|'.$row->external_campaign_id);

        if ($this->adops->isAvailable()) {
            CampaignPerformanceSuggestion::query()
                ->where('platform', 'meta')
                ->get()
                ->each(function (CampaignPerformanceSuggestion $row) use ($suggestions): void {
                    if (data_get($row->snapshot, 'data_source') !== 'whale_adops') {
                        return;
                    }

                    $suggestions->put($row->platform.'|'.$row->external_campaign_id, $row);
                });
        }

        $learningWindow = (int) config('campaign-performance.learning_window_days', 3);
        $maxTest = (float) config('campaign-performance.max_test_spend', 40);
        $ingestErrors = [];

        return $identities->map(function (array $identity) use ($suggestions, $learningWindow, $maxTest, $whaleTotals, &$ingestErrors): array {
            $key = $identity['platform'].'|'.$identity['external_campaign_id'];
            $suggestion = $suggestions->get($key);
            $daysLive = max(1, (int) ($suggestion?->snapshot['days_live'] ?? 1));
            $lifetimeSpend = (float) data_get($suggestion?->snapshot, 'lifetime.spend', 0);
            $spend = (float) data_get($suggestion?->snapshot, 'last_7_days.spend', 0);
            $clicks = (int) data_get($suggestion?->snapshot, 'last_7_days.clicks', 0);
            $impressions = (int) data_get($suggestion?->snapshot, 'last_7_days.impressions', 0);
            $platformConversions = (int) data_get($suggestion?->snapshot, 'last_7_days.conversions', 0);
            $platformConversionValue = (float) data_get($suggestion?->snapshot, 'last_7_days.conversion_value', 0);

            if ($identity['platform'] === 'meta' && isset($whaleTotals[$identity['external_campaign_id']])) {
                $whale = $whaleTotals[$identity['external_campaign_id']];
                $spend = round((float) $whale['spend'], 2);
                $clicks = (int) $whale['clicks'];
                $impressions = (int) $whale['impressions'];
            } else {
                $spend = round($spend, 2);
            }

            $name = $this->resolveCampaignName(
                $identity['external_campaign_id'],
                (string) $identity['name'],
                $suggestion,
            );
            $ctr = $impressions > 0 ? round($clicks / $impressions, 4) : null;

            return [
                'name' => $name,
                'platform' => $identity['platform'],
                'external_campaign_id' => $identity['external_campaign_id'],
                'account_id' => $identity['account_id'] ?? null,
                'daily_budget' => $identity['daily_budget'] ?? data_get($suggestion?->snapshot, 'daily_budget'),
                'started_at' => $identity['started_at'] ?? data_get($suggestion?->snapshot, 'campaign_profile.started_at'),
                'platform_status' => $identity['platform_status'] ?? data_get($suggestion?->snapshot, 'platform_status'),
                'delivery_status' => $identity['learning_status'] ?? data_get($suggestion?->snapshot, 'delivery_status'),
                'region_label' => data_get($suggestion?->snapshot, 'campaign_profile.region_label'),
                'platform_learning_label' => $identity['learning_status']
                    ?? data_get($suggestion?->snapshot, 'campaign_profile.platform_learning_label'),
                'status' => $suggestion?->status,
                'verdict' => $this->resolvedVerdict($suggestion?->status)?->value ?? $suggestion?->status,
                'verdict_label' => $this->resolvedVerdict($suggestion?->status)?->label() ?? 'No verdict yet',
                'action_summary' => $this->resolvedVerdictActionSummary($suggestion?->status),
                'learning' => $this->learningLabel($daysLive, $learningWindow, $lifetimeSpend > 0 ? $lifetimeSpend : $spend, $maxTest),
                'spend_7d' => $spend,
                'clicks_7d' => $clicks,
                'impressions_7d' => $impressions,
                'cpc_7d' => $clicks > 0 ? round($spend / $clicks, 4) : null,
                'ctr_7d' => $ctr,
                'platform_conversions_7d' => $platformConversions,
                'platform_conversion_value_7d' => $platformConversionValue,
                'affiliate_clicks_7d' => data_get($suggestion?->snapshot, 'affiliate.7d.clicks'),
                'affiliate_sum_cpc_7d' => data_get($suggestion?->snapshot, 'affiliate.7d.sum_cpc'),
                'affiliate_orders_7d' => data_get($suggestion?->snapshot, 'affiliate.7d.orders'),
                'affiliate_matched' => (bool) data_get($suggestion?->snapshot, 'affiliate.matched', false),
                'trend' => data_get($suggestion?->snapshot, 'trend'),
                'trend_summary' => data_get($suggestion?->snapshot, 'trend_summary'),
                'reason' => $suggestion?->reason,
                'verdict_reason' => $this->engineVerdictReason($suggestion),
                'verdict_reasons' => $this->engineVerdictReasons($suggestion),
                'affiliate_net_revenue_7d' => data_get($suggestion?->snapshot, 'affiliate.7d.net_revenue'),
                'updated_at' => $suggestion?->suggested_at,
                'last_error' => $this->ingestErrorFor($identity, $ingestErrors),
            ];
        })->sort(function (array $left, array $right): int {
            $spend = $right['spend_7d'] <=> $left['spend_7d'];

            return $spend !== 0 ? $spend : strcasecmp((string) $left['name'], (string) $right['name']);
        })->values();
    }

    /**
     * @return Collection<int, array{platform: string, external_campaign_id: string, name: string, account_id: ?string}>
     */
    private function identities(User $user): Collection
    {
        $rows = collect();

        if ($this->adops->isAvailable() && config('campaign-performance.adops_list_all')) {
            foreach ($this->adops->campaigns('meta') as $campaign) {
                $this->putIdentity(
                    $rows,
                    $campaign['platform'] !== '' ? $campaign['platform'] : 'meta',
                    $campaign['campaign_id'],
                    $campaign['name'] !== '' ? $campaign['name'] : 'Campaign '.$campaign['campaign_id'],
                    [
                        'account_id' => $campaign['account_id'],
                        'platform_status' => $campaign['effective_status'] ?? $campaign['status'],
                        'daily_budget' => $campaign['daily_budget'],
                        'learning_status' => $campaign['learning_status'],
                        'started_at' => $campaign['started_at'],
                    ],
                );
            }
        }

        $this->restrictToUser(CampaignPerformanceSuggestion::query(), $user)
            ->get()
            ->each(function (CampaignPerformanceSuggestion $suggestion) use ($rows): void {
                $this->putIdentity(
                    $rows,
                    (string) $suggestion->platform,
                    (string) $suggestion->external_campaign_id,
                    'Campaign '.$suggestion->external_campaign_id,
                    [
                        'account_id' => data_get($suggestion->snapshot, 'campaign_profile.account_id'),
                        'platform_status' => data_get($suggestion->snapshot, 'platform_status'),
                        'daily_budget' => data_get($suggestion->snapshot, 'daily_budget'),
                        'learning_status' => data_get($suggestion->snapshot, 'campaign_profile.platform_learning_label'),
                        'started_at' => data_get($suggestion->snapshot, 'campaign_profile.started_at'),
                    ],
                );
            });

        return $rows->values();
    }

    /**
     * @param  Collection<string, array<string, mixed>>  $rows
     * @param  array<string, mixed>  $meta
     */
    private function putIdentity(Collection $rows, string $platform, string $externalId, string $name, array $meta = []): void
    {
        $key = $platform.'|'.$externalId;

        if ($rows->has($key)) {
            return;
        }

        $accountId = $meta['account_id'] ?? null;

        $rows->put($key, [
            'platform' => $platform,
            'external_campaign_id' => $externalId,
            'name' => $name,
            'account_id' => is_string($accountId) && $accountId !== '' ? $accountId : null,
            'platform_status' => is_string($meta['platform_status'] ?? null) ? $meta['platform_status'] : null,
            'daily_budget' => isset($meta['daily_budget']) ? (is_numeric($meta['daily_budget']) ? (float) $meta['daily_budget'] : null) : null,
            'learning_status' => is_string($meta['learning_status'] ?? null) ? $meta['learning_status'] : null,
            'started_at' => is_string($meta['started_at'] ?? null) ? $meta['started_at'] : null,
        ]);
    }

    /**
     * @param  array{platform: string, account_id?: ?string}  $identity
     * @param  array<string, ?string>  $cache
     */
    private function ingestErrorFor(array $identity, array &$cache): ?string
    {
        $accountId = is_string($identity['account_id'] ?? null) ? $identity['account_id'] : null;
        $key = $identity['platform'].'|'.($accountId ?? '');

        if (! array_key_exists($key, $cache)) {
            $cache[$key] = $this->adops->ingestError($identity['platform'], $accountId, 'hourly');
        }

        return $cache[$key];
    }

    private function resolveCampaignName(
        string $externalId,
        string $identityName,
        ?CampaignPerformanceSuggestion $suggestion,
    ): string {
        $candidates = [
            data_get($suggestion?->snapshot, 'campaign'),
            data_get($suggestion?->snapshot, 'campaign_profile.name'),
            $identityName,
        ];

        foreach ($candidates as $candidate) {
            $name = trim((string) ($candidate ?? ''));

            if ($name !== '' && ! $this->isPlaceholderCampaignName($name, $externalId)) {
                return $name;
            }
        }

        return 'Campaign '.$externalId;
    }

    private function isPlaceholderCampaignName(string $name, string $externalId): bool
    {
        return $name === 'Campaign '.$externalId;
    }

    private function learningLabel(int $daysLive, int $window, float $spend, float $maxTest): string
    {
        $spendText = '$'.number_format($spend, 2);
        $capText = '$'.number_format($maxTest, 2);

        if ($daysLive < $window) {
            return sprintf('Learning day %d of %d · %s of %s test spend', $daysLive, $window, $spendText, $capText);
        }

        return sprintf('%d days live · %s spend vs %s test cap', $daysLive, $spendText, $capText);
    }

    /**
     * @param  array{
     *     log_run_type?: string,
     *     log_verdict?: string,
     *     log_days?: int,
     *     log_per_page?: int
     * }  $filters
     */
    private function verdictLogsFor(User $user, string $platform, string $externalId, array $filters): LengthAwarePaginator
    {
        $query = $this->restrictToUser(CampaignPerformanceVerdictLog::query(), $user)
            ->where('platform', $platform)
            ->where('external_campaign_id', $externalId);

        $runType = trim((string) ($filters['log_run_type'] ?? ''));

        if (in_array($runType, ['hourly', 'daily', 'manual'], true)) {
            $query->where('run_type', $runType);
        }

        $verdict = trim((string) ($filters['log_verdict'] ?? ''));

        if (PerformanceVerdict::tryFromStored($verdict) !== null) {
            $query->where('final_verdict', $verdict);
        }

        $days = (int) ($filters['log_days'] ?? 0);

        if (in_array($days, [7, 30, 90], true)) {
            $query->where('evaluated_at', '>=', now()->subDays($days));
        }

        $perPage = in_array((int) ($filters['log_per_page'] ?? 10), [10, 25, 50], true)
            ? (int) $filters['log_per_page']
            : 10;

        return $query
            ->orderByDesc('evaluated_at')
            ->orderByDesc('id')
            ->paginate($perPage, ['*'], 'log_page')
            ->withQueryString();
    }

    private function resolvedVerdict(?string $status): CampaignDecisionVerdict|PerformanceVerdict|null
    {
        return CampaignDecisionVerdict::tryFromStored($status)
            ?? PerformanceVerdict::tryFromStored($status);
    }

    private function resolvedVerdictActionSummary(?string $status): string
    {
        $verdict = $this->resolvedVerdict($status);

        if ($verdict instanceof CampaignDecisionVerdict) {
            return $verdict->actionSummary();
        }

        if ($verdict instanceof PerformanceVerdict) {
            return $verdict->actionSummary();
        }

        return 'Run campaigns:monitor-adops --hourly to generate a suggestion.';
    }

    private function engineVerdictReason(?CampaignPerformanceSuggestion $suggestion): ?string
    {
        if ($suggestion === null) {
            return null;
        }

        $snapshot = is_array($suggestion->snapshot) ? $suggestion->snapshot : [];

        if (data_get($snapshot, 'decision_engine') === 'v2') {
            $reason = data_get($snapshot, 'reason');

            return is_string($reason) && $reason !== '' ? $reason : null;
        }

        $reason = data_get($snapshot, 'final_reason')
            ?: data_get($snapshot, 'verdicts.final_reason');

        return is_string($reason) && $reason !== '' ? $reason : null;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function engineVerdictReasons(?CampaignPerformanceSuggestion $suggestion): array
    {
        if ($suggestion === null) {
            return [];
        }

        $reasons = data_get($suggestion->snapshot, 'reasons', []);

        return is_array($reasons) ? array_values(array_filter($reasons, is_array(...))) : [];
    }

    /**
     * @param  Builder<Model>  $query
     * @return Builder<Model>
     */
    private function restrictToUser(Builder $query, User $user): Builder
    {
        if (config('campaign-performance.skip_user_scope')) {
            return $query;
        }

        return $query->where('user_id', $user->id);
    }
}
