<?php

namespace App\Services\Meta;

use App\Exceptions\MetaMarketingApiException;
use App\Models\MetaAdsInsight;
use App\Models\MetaAdsSyncLog;
use Carbon\CarbonInterface;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Throwable;

class MetaAdsInsightsSyncService
{
    private const FIELDS = [
        'account_currency',
        'campaign_id',
        'campaign_name',
        'adset_id',
        'adset_name',
        'ad_id',
        'ad_name',
        'impressions',
        'clicks',
        'spend',
        'actions',
        'action_values',
        'attribution_setting',
    ];

    public function sync(CarbonInterface $from, CarbonInterface $to): MetaAdsSyncLog
    {
        $this->assertReady();

        if ($from->gt($to)) {
            throw new RuntimeException('Meta Insights start date must not be after the end date.');
        }

        if ($from->diffInDays($to) > 180) {
            throw new RuntimeException('A single Meta Insights sync is limited to 180 days.');
        }

        $log = MetaAdsSyncLog::query()->create([
            'date_from' => $from->toDateString(),
            'date_to' => $to->toDateString(),
            'status' => 'processing',
            'started_at' => now(),
        ]);

        $warnings = [];

        try {
            $baseRows = $this->fetch($from, $to, false);
            $this->replaceRange($from, $to, false, $baseRows);

            $productRows = [];
            try {
                $productRows = $this->fetch($from, $to, true);
                $this->replaceRange($from, $to, true, $productRows);
            } catch (Throwable $exception) {
                $warnings[] = [
                    'stage' => 'product_breakdown',
                    'message' => $exception->getMessage(),
                    'meta_error' => $exception instanceof MetaMarketingApiException
                        ? $exception->logContext()
                        : null,
                ];
            }

            $log->fill([
                'status' => $warnings === [] ? 'completed' : 'completed_with_warnings',
                'base_rows' => count($baseRows),
                'product_rows' => count($productRows),
                'warnings' => $warnings,
                'finished_at' => now(),
            ])->save();

            return $log;
        } catch (Throwable $exception) {
            $context = $exception instanceof MetaMarketingApiException
                ? $exception->logContext()
                : [];

            $log->fill([
                'status' => 'failed',
                'warnings' => $warnings,
                'meta_http_status' => data_get($context, 'http_status'),
                'meta_error_code' => data_get($context, 'code'),
                'meta_error_subcode' => data_get($context, 'subcode'),
                'meta_trace_id' => data_get($context, 'trace_id'),
                'error_message' => $exception->getMessage(),
                'finished_at' => now(),
            ])->save();

            throw $exception;
        }
    }

    private function fetch(CarbonInterface $from, CarbonInterface $to, bool $productBreakdown): array
    {
        $url = $this->graphUrl('/' . $this->adAccountId() . '/insights');
        $query = [
            'access_token' => $this->accessToken(),
            'level' => 'ad',
            'fields' => implode(',', self::FIELDS),
            'time_increment' => 1,
            'time_range' => json_encode([
                'since' => $from->toDateString(),
                'until' => $to->toDateString(),
            ], JSON_UNESCAPED_SLASHES),
            'limit' => 5000,
            'action_report_time' => 'conversion',
        ];

        if ($productBreakdown) {
            $query['breakdowns'] = 'product_id';
        }

        $rows = [];

        for ($page = 0; $page < 100 && $url !== ''; $page++) {
            $response = Http::timeout($this->timeout())->get($url, $query);
            $this->assertSuccessful('Meta Ads Insights sync', $response);

            foreach ((array) data_get($response->json(), 'data', []) as $row) {
                $normalized = $this->normalizeRow((array) $row, $productBreakdown);
                if ($normalized !== null) {
                    $rows[] = $normalized;
                }
            }

            $next = trim((string) data_get($response->json(), 'paging.next'));
            $url = str_starts_with($next, 'https://graph.facebook.com/') ? $next : '';
            $query = [];
        }

        return $rows;
    }

    private function normalizeRow(array $row, bool $productBreakdown): ?array
    {
        $date = trim((string) data_get($row, 'date_start'));
        $adId = trim((string) data_get($row, 'ad_id'));
        $productId = $productBreakdown ? trim((string) data_get($row, 'product_id')) : '';

        if ($date === '' || $adId === '' || ($productBreakdown && $productId === '')) {
            return null;
        }

        $identity = [
            $date,
            'ad',
            (string) data_get($row, 'campaign_id', ''),
            (string) data_get($row, 'adset_id', ''),
            $adId,
            $productId,
        ];

        return [
            'identity_hash' => hash('sha256', implode('|', $identity)),
            'insight_date' => $date,
            'level' => 'ad',
            'campaign_id' => $this->nullableString(data_get($row, 'campaign_id'), 100),
            'campaign_name' => $this->nullableString(data_get($row, 'campaign_name')),
            'adset_id' => $this->nullableString(data_get($row, 'adset_id'), 100),
            'adset_name' => $this->nullableString(data_get($row, 'adset_name')),
            'ad_id' => $adId,
            'ad_name' => $this->nullableString(data_get($row, 'ad_name')),
            'product_id' => $productId !== '' ? $productId : null,
            'impressions' => max(0, (int) data_get($row, 'impressions', 0)),
            'clicks' => max(0, (int) data_get($row, 'clicks', 0)),
            'add_to_cart' => $this->actionValue((array) data_get($row, 'actions', []), [
                'offsite_conversion.fb_pixel_add_to_cart',
                'omni_add_to_cart',
                'add_to_cart',
            ]),
            'purchases' => $this->actionValue((array) data_get($row, 'actions', []), [
                'offsite_conversion.fb_pixel_purchase',
                'omni_purchase',
                'purchase',
            ]),
            'spend' => max(0, (float) data_get($row, 'spend', 0)),
            'purchase_value' => $this->actionValue((array) data_get($row, 'action_values', []), [
                'offsite_conversion.fb_pixel_purchase',
                'omni_purchase',
                'purchase',
            ]),
            'currency' => $this->nullableString(data_get($row, 'account_currency'), 10),
            'attribution_setting' => $this->nullableString(data_get($row, 'attribution_setting'), 100),
            'raw_response' => json_encode($row, JSON_UNESCAPED_SLASHES),
            'synced_at' => now(),
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }

    private function replaceRange(
        CarbonInterface $from,
        CarbonInterface $to,
        bool $productBreakdown,
        array $rows
    ): void {
        DB::transaction(function () use ($from, $to, $productBreakdown, $rows) {
            MetaAdsInsight::query()
                ->whereBetween('insight_date', [$from->toDateString(), $to->toDateString()])
                ->when(
                    $productBreakdown,
                    fn ($query) => $query->whereNotNull('product_id'),
                    fn ($query) => $query->whereNull('product_id')
                )
                ->delete();

            foreach (array_chunk($rows, 500) as $chunk) {
                MetaAdsInsight::query()->upsert(
                    $chunk,
                    ['identity_hash'],
                    [
                        'campaign_id',
                        'campaign_name',
                        'adset_id',
                        'adset_name',
                        'ad_id',
                        'ad_name',
                        'product_id',
                        'impressions',
                        'clicks',
                        'add_to_cart',
                        'purchases',
                        'spend',
                        'purchase_value',
                        'currency',
                        'attribution_setting',
                        'raw_response',
                        'synced_at',
                        'updated_at',
                    ]
                );
            }
        });
    }

    private function actionValue(array $actions, array $priority): float
    {
        $indexed = collect($actions)
            ->filter(fn ($action) => is_array($action) && filled(data_get($action, 'action_type')))
            ->keyBy(fn ($action) => (string) data_get($action, 'action_type'));

        foreach ($priority as $actionType) {
            if ($indexed->has($actionType)) {
                return max(0, (float) data_get($indexed->get($actionType), 'value', 0));
            }
        }

        return 0.0;
    }

    private function assertReady(): void
    {
        if (! Schema::hasTable('meta_ads_insights') || ! Schema::hasTable('meta_ads_sync_logs')) {
            throw new RuntimeException('Meta Ads performance tables are missing. Run migrations first.');
        }

        if ($this->accessToken() === '' || $this->adAccountId() === '') {
            throw new RuntimeException('Meta Marketing API access token and Ad Account ID are required.');
        }
    }

    private function assertSuccessful(string $operation, Response $response): void
    {
        if (! $response->successful()) {
            throw MetaMarketingApiException::fromResponse($operation, $response);
        }
    }

    private function graphUrl(string $path): string
    {
        $version = trim((string) config('services.meta_marketing.graph_version', 'v20.0'));
        $version = str_starts_with($version, 'v') ? $version : 'v' . $version;

        return 'https://graph.facebook.com/' . $version . $path;
    }

    private function accessToken(): string
    {
        return trim((string) config('services.meta_marketing.access_token'));
    }

    private function adAccountId(): string
    {
        $id = trim((string) config('services.meta_marketing.ad_account_id'));

        return $id !== '' && ! str_starts_with($id, 'act_') ? 'act_' . $id : $id;
    }

    private function timeout(): int
    {
        return max(5, (int) config('services.meta_marketing.timeout', 15));
    }

    private function nullableString(mixed $value, int $maxLength = 255): ?string
    {
        $value = trim((string) $value);

        return $value !== '' ? mb_substr($value, 0, $maxLength) : null;
    }
}
