<?php

namespace App\Campaigns;

use App\Models\Campaign;
use App\Services\Meta\Meta;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Throwable;

class MetaPerformanceReader
{
    /** @var array<string, array<string, mixed>> */
    private array $campaigns = [];

    public function __construct(private readonly Meta $meta) {}

    public function startedOn(string $campaignId): ?string
    {
        $campaign = $this->campaign($campaignId);

        foreach (['start_time', 'created_time'] as $field) {
            $value = $campaign[$field] ?? null;

            if (! is_string($value) || $value === '') {
                continue;
            }

            return Carbon::parse($value)->toDateString();
        }

        return null;
    }

    /**
     * @return list<array{
     *     stat_date: string,
     *     spend: float,
     *     impressions: int,
     *     clicks: int,
     *     ctr: ?float,
     *     cpc: ?float,
     *     platform_status: ?string,
     *     delivery_status: ?string,
     *     platform_payload: array<string, mixed>
     * }>
     */
    public function days(Campaign $campaign, string $since, string $until): array
    {
        return $this->daysFor((string) $campaign->external_campaign_id, $since, $until);
    }

    /**
     * @return list<array{
     *     stat_date: string,
     *     spend: float,
     *     impressions: int,
     *     clicks: int,
     *     ctr: ?float,
     *     cpc: ?float,
     *     platform_status: ?string,
     *     delivery_status: ?string,
     *     platform_payload: array<string, mixed>
     * }>
     */
    public function daysFor(string $campaignId, string $since, string $until): array
    {
        $insights = $this->meta->insights($campaignId, $since, $until);
        $status = $this->statusFrom($this->campaign($campaignId));

        return array_values(array_filter(array_map(
            fn (array $row): ?array => $this->mapDay($row, $status),
            $insights,
        )));
    }

    /**
     * @return list<array{
     *     stat_hour: string,
     *     spend: float,
     *     impressions: int,
     *     clicks: int,
     *     ctr: ?float,
     *     cpc: ?float,
     *     platform_status: ?string,
     *     delivery_status: ?string,
     *     platform_payload: array<string, mixed>
     * }>
     */
    public function hoursFor(string $campaignId, CarbonInterface $since, CarbonInterface $until): array
    {
        $insights = $this->meta->insightsHourly(
            $campaignId,
            $since->toDateString(),
            $until->toDateString(),
        );
        $status = $this->statusFrom($this->campaign($campaignId));

        return array_values(array_filter(array_map(
            fn (array $row): ?array => $this->mapHour($row, $status),
            $insights,
        )));
    }

    /**
     * @param  array<string, mixed>  $row
     * @return array{
     *     stat_date: string,
     *     spend: float,
     *     impressions: int,
     *     clicks: int,
     *     ctr: ?float,
     *     cpc: ?float,
     *     platform_status: ?string,
     *     delivery_status: ?string,
     *     platform_payload: array<string, mixed>
     * }|null
     */
    private function mapDay(array $row, ?string $status): ?array
    {
        $date = (string) ($row['date_start'] ?? $row['date_stop'] ?? '');

        if ($date === '') {
            return null;
        }

        $impressions = $this->unsigned($row['impressions'] ?? 0);
        $clicks = $this->unsigned($row['clicks'] ?? 0);
        $spend = $this->decimal($row['spend'] ?? 0);

        return [
            'stat_date' => $date,
            'spend' => $spend,
            'impressions' => $impressions,
            'clicks' => $clicks,
            'ctr' => $this->ctr($clicks, $impressions, $row['ctr'] ?? null),
            'cpc' => $this->cpc($spend, $clicks, $row['cpc'] ?? null),
            'platform_status' => $status,
            'delivery_status' => null,
            'platform_payload' => $row,
        ];
    }

    /**
     * @param  array<string, mixed>  $row
     * @return array{
     *     stat_hour: string,
     *     spend: float,
     *     impressions: int,
     *     clicks: int,
     *     ctr: ?float,
     *     cpc: ?float,
     *     platform_status: ?string,
     *     delivery_status: ?string,
     *     platform_payload: array<string, mixed>
     * }|null
     */
    private function mapHour(array $row, ?string $status): ?array
    {
        $hour = $this->hourFrom($row);

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

        $impressions = $this->unsigned($row['impressions'] ?? 0);
        $clicks = $this->unsigned($row['clicks'] ?? 0);
        $spend = $this->decimal($row['spend'] ?? 0);

        return [
            'stat_hour' => $hour->toDateTimeString(),
            'spend' => $spend,
            'impressions' => $impressions,
            'clicks' => $clicks,
            'ctr' => $this->ctr($clicks, $impressions, $row['ctr'] ?? null),
            'cpc' => $this->cpc($spend, $clicks, $row['cpc'] ?? null),
            'platform_status' => $status,
            'delivery_status' => null,
            'platform_payload' => $row,
        ];
    }

    /**
     * @param  array<string, mixed>  $row
     */
    private function hourFrom(array $row): ?Carbon
    {
        $bucket = (string) ($row['hourly_stats_aggregated_by_advertiser_time_zone']
            ?? $row['hourly_stats_aggregated_by_audience_time_zone']
            ?? '');
        $date = (string) ($row['date_start'] ?? $row['date_stop'] ?? '');

        if ($bucket !== '' && preg_match('/^(\d{2}):(\d{2})/', $bucket, $matches) === 1) {
            $day = preg_match('/^\d{4}-\d{2}-\d{2}/', $date) === 1
                ? substr($date, 0, 10)
                : now()->toDateString();

            return Carbon::parse($day.' '.$matches[1].':'.$matches[2].':00')->startOfHour();
        }

        if ($date !== '' && preg_match('/\d{2}:\d{2}/', $date) === 1) {
            return Carbon::parse($date)->startOfHour();
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    private function campaign(string $campaignId): array
    {
        if (isset($this->campaigns[$campaignId])) {
            return $this->campaigns[$campaignId];
        }

        try {
            $this->campaigns[$campaignId] = $this->meta->campaign($campaignId);
        } catch (Throwable) {
            $this->campaigns[$campaignId] = [];
        }

        return $this->campaigns[$campaignId];
    }

    /**
     * @param  array<string, mixed>  $campaign
     */
    private function statusFrom(array $campaign): ?string
    {
        $effective = $campaign['effective_status'] ?? null;
        $status = $campaign['status'] ?? null;

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

    private function unsigned(mixed $value): int
    {
        return max(0, (int) $value);
    }

    private function decimal(mixed $value): float
    {
        return round((float) $value, 2);
    }

    private function ctr(int $clicks, int $impressions, mixed $raw): ?float
    {
        if ($impressions > 0) {
            return round($clicks / $impressions, 4);
        }

        if ($raw === null || $raw === '') {
            return null;
        }

        $text = rtrim((string) $raw, '%');

        return is_numeric($text) ? round((float) $text, 4) : null;
    }

    private function cpc(float $spend, int $clicks, mixed $raw): ?float
    {
        if ($clicks > 0) {
            return round($spend / $clicks, 4);
        }

        if ($raw === null || $raw === '' || ! is_numeric($raw)) {
            return null;
        }

        return round((float) $raw, 4);
    }
}
