<?php

namespace App\Services\Meta;

use App\Models\Item;
use App\Models\MetaAdsInsight;
use App\Models\MetaOrderAttribution;
use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\PurchaseItem;
use App\Services\Customer\CustomerQualityService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;

class MetaAdsPerformanceService
{
    public function __construct(
        private readonly MetaAdsMetricsCalculator $calculator,
        private readonly CustomerQualityService $customerQualityService
    ) {
    }

    public function report(Request $request): array
    {
        [$from, $to] = $this->dateRange($request);
        $insights = Schema::hasTable('meta_ads_insights')
            ? MetaAdsInsight::query()
                ->whereBetween('insight_date', [$from->toDateString(), $to->toDateString()])
                ->get()
            : collect();
        $local = $this->localMetrics($from, $to);

        $baseInsights = $insights->whereNull('product_id');
        $productInsights = $insights->whereNotNull('product_id');

        $summaryInsight = $this->aggregateInsights($baseInsights);
        $summaryLocal = $local['summary'];
        $summary = array_replace(
            $summaryInsight,
            $summaryLocal,
            $this->calculator->calculate(
                $summaryInsight['spend'],
                $summaryInsight['purchase_value'],
                $summaryLocal['attributed_order_value'],
                $summaryLocal['delivered_revenue'],
                $summaryLocal['returned_value'],
                $summaryLocal['cancelled_value'],
                $summaryLocal['delivered_orders'],
                $summaryLocal['returned_orders'],
                $summaryLocal['profit']
            )
        );

        $campaignRows = $this->campaignRows($baseInsights, $local['campaigns']);
        $adsetRows = $this->entityRows(
            $baseInsights,
            $local['adsets'],
            'adset_id',
            'adset_name',
            'Ad set'
        );
        $adRows = $this->entityRows(
            $baseInsights,
            $local['ads'],
            'ad_id',
            'ad_name',
            'Ad'
        );
        $productRows = $this->productRows($productInsights, $local['products']);
        $lastSync = Schema::hasTable('meta_ads_sync_logs')
            ? \App\Models\MetaAdsSyncLog::query()->latest('id')->first()
            : null;

        return [
            'filters' => [
                'from' => $from->toDateString(),
                'to' => $to->toDateString(),
            ],
            'summary' => $summary,
            'campaign_rows' => $campaignRows,
            'adset_rows' => $adsetRows,
            'ad_rows' => $adRows,
            'product_rows' => $productRows,
            'last_sync' => $lastSync,
            'attribution_coverage' => [
                'attributed_orders' => $summaryLocal['attributed_order_count'],
                'total_orders' => $this->totalOrders($from, $to),
            ],
            'missing_purchase_cost_items' => $local['missing_purchase_cost_items'],
        ];
    }

    private function campaignRows(Collection $insights, array $localRows): Collection
    {
        $insightRows = $insights
            ->groupBy(fn ($row) => $row->campaign_id ?: '__unknown__')
            ->map(function (Collection $rows, string $key) {
                return array_replace($this->aggregateInsights($rows), [
                    'key' => $key,
                    'campaign_id' => $key === '__unknown__' ? null : $key,
                    'campaign_name' => (string) ($rows->firstWhere('campaign_name', '!=', null)?->campaign_name ?: 'Unknown campaign'),
                ]);
            })
            ->all();

        $keys = collect(array_keys($insightRows))
            ->merge(array_keys($localRows))
            ->unique();

        return $keys->map(function (string $key) use ($insightRows, $localRows) {
            $insight = $insightRows[$key] ?? array_replace($this->emptyInsight(), [
                'key' => $key,
                'campaign_id' => $key === '__unknown__' ? null : $key,
                'campaign_name' => $key === '__unknown__' ? 'Meta click without campaign ID' : $key,
            ]);
            $local = $localRows[$key] ?? $this->emptyLocal();

            return array_replace(
                $insight,
                $local,
                $this->calculator->calculate(
                    $insight['spend'],
                    $insight['purchase_value'],
                    $local['attributed_order_value'],
                    $local['delivered_revenue'],
                    $local['returned_value'],
                    $local['cancelled_value'],
                    $local['delivered_orders'],
                    $local['returned_orders'],
                    $local['profit']
                )
            );
        })->sortByDesc('spend')->values();
    }

    private function entityRows(
        Collection $insights,
        array $localRows,
        string $idField,
        string $nameField,
        string $fallbackLabel
    ): Collection {
        $insightRows = $insights
            ->groupBy(fn ($row) => $row->{$idField} ?: '__unknown__')
            ->map(function (Collection $rows, string $key) use ($idField, $nameField, $fallbackLabel) {
                $name = (string) ($rows->firstWhere($nameField, '!=', null)?->{$nameField}
                    ?: ($key === '__unknown__' ? "Unknown {$fallbackLabel}" : "{$fallbackLabel} {$key}"));

                return array_replace($this->aggregateInsights($rows), [
                    'key' => $key,
                    $idField => $key === '__unknown__' ? null : $key,
                    $nameField => $name,
                ]);
            })
            ->all();

        $keys = collect(array_keys($insightRows))
            ->merge(array_keys($localRows))
            ->unique();

        return $keys->map(function (string $key) use (
            $insightRows,
            $localRows,
            $idField,
            $nameField,
            $fallbackLabel
        ) {
            $insight = $insightRows[$key] ?? array_replace($this->emptyInsight(), [
                'key' => $key,
                $idField => $key === '__unknown__' ? null : $key,
                $nameField => $key === '__unknown__'
                    ? "Meta click without {$fallbackLabel} ID"
                    : "{$fallbackLabel} {$key}",
            ]);
            $local = $localRows[$key] ?? $this->emptyLocal();

            return array_replace(
                $insight,
                $local,
                $this->calculator->calculate(
                    $insight['spend'],
                    $insight['purchase_value'],
                    $local['attributed_order_value'],
                    $local['delivered_revenue'],
                    $local['returned_value'],
                    $local['cancelled_value'],
                    $local['delivered_orders'],
                    $local['returned_orders'],
                    $local['profit']
                )
            );
        })->sortByDesc('spend')->values();
    }

    private function productRows(Collection $insights, array $localRows): Collection
    {
        $insightRows = $insights
            ->groupBy(fn ($row) => (string) $row->product_id)
            ->map(function (Collection $rows, string $key) {
                return array_replace($this->aggregateInsights($rows), [
                    'key' => $key,
                    'product_id' => $key,
                ]);
            })
            ->all();

        $keys = collect(array_keys($insightRows))
            ->merge(array_keys($localRows))
            ->filter()
            ->unique();
        $numericIds = $keys
            ->filter(fn ($id) => ctype_digit((string) $id))
            ->map(fn ($id) => (int) $id)
            ->all();
        $items = collect();
        if ($numericIds !== []) {
            $columns = ['id', 'name'];
            foreach ([
                'meta_ad_priority',
                'meta_ads_high_profit',
                'meta_ads_excluded',
                'meta_ads_exclusion_mode',
                'meta_ads_exclusion_reason',
                'meta_ads_excluded_at',
            ] as $column) {
                if (Schema::hasColumn('items', $column)) {
                    $columns[] = $column;
                }
            }

            $items = Item::withoutGlobalScopes()
                ->whereIn('id', $numericIds)
                ->get($columns)
                ->keyBy('id');
        }

        return $keys->map(function (string $key) use ($insightRows, $localRows, $items) {
            $insight = $insightRows[$key] ?? array_replace($this->emptyInsight(), [
                'key' => $key,
                'product_id' => $key,
            ]);
            $local = $localRows[$key] ?? $this->emptyLocal();
            $item = ctype_digit($key) ? $items->get((int) $key) : null;
            $productName = $local['product_name']
                ?? $item?->name
                ?? "Catalog product {$key}";

            return array_replace(
                $insight,
                $local,
                [
                    'product_name' => $productName,
                    'item_exists' => $item !== null,
                    'meta_ad_priority' => (string) ($item?->meta_ad_priority ?: 'medium'),
                    'meta_ads_high_profit' => (bool) ($item?->meta_ads_high_profit ?? false),
                    'meta_ads_excluded' => (bool) ($item?->meta_ads_excluded ?? false),
                    'meta_ads_exclusion_mode' => $item?->meta_ads_exclusion_mode,
                    'meta_ads_exclusion_reason' => $item?->meta_ads_exclusion_reason,
                ],
                $this->calculator->calculate(
                    $insight['spend'],
                    $insight['purchase_value'],
                    $local['attributed_order_value'],
                    $local['delivered_revenue'],
                    $local['returned_value'],
                    $local['cancelled_value'],
                    $local['delivered_orders'],
                    $local['returned_orders'],
                    $local['profit']
                )
            );
        })->sortByDesc('spend')->values();
    }

    private function localMetrics(Carbon $from, Carbon $to): array
    {
        $empty = [
            'summary' => $this->emptyLocal(),
            'campaigns' => [],
            'adsets' => [],
            'ads' => [],
            'products' => [],
            'missing_purchase_cost_items' => 0,
        ];

        if (! Schema::hasTable('meta_order_attributions') || ! Schema::hasTable('orders')) {
            return $empty;
        }

        $attributions = MetaOrderAttribution::query()
            ->with([
                'order.details.item',
                'order.details.campaign',
                'order.courierShipment',
            ])
            ->whereHas('order', fn ($query) => $query->whereBetween('created_at', [$from, $to]))
            ->get();
        $purchaseCosts = $this->averagePurchaseCostMap($to);
        $missingPurchaseCosts = [];
        $summary = $this->emptyLocal();
        $campaigns = [];
        $adsets = [];
        $ads = [];
        $products = [];
        $deliveredStatuses = $this->statusSet('delivered');
        $returnedStatuses = $this->statusSet('returned');
        $cancelledStatuses = $this->statusSet('cancelled');

        foreach ($attributions as $attribution) {
            $order = $attribution->order;
            if (! $order || $order->order_type === 'parcel') {
                continue;
            }

            $status = $this->normalizeStatus((string) $order->order_status);
            $isDelivered = in_array($status, $deliveredStatuses, true);
            $isReturned = in_array($status, $returnedStatuses, true);
            $isCancelled = in_array($status, $cancelledStatuses, true);
            $orderValue = max(0, (float) $order->order_amount);
            $deliveredRevenue = $isDelivered ? $this->deliveredRevenue($order, $status) : 0.0;
            $returnedValue = $isReturned ? $orderValue : 0.0;
            $cancelledValue = $isCancelled ? $orderValue : 0.0;
            $details = $this->detailRows($order->details);
            $deliveryCost = $isDelivered ? max(0, (float) data_get($order, 'courierShipment.actual_delivery_cost', 0)) : 0.0;
            $codCharge = $isDelivered ? max(0, (float) data_get($order, 'courierShipment.actual_cod_charge', 0)) : 0.0;
            $returnCost = $isReturned ? max(0, (float) data_get($order, 'courierShipment.actual_return_cost', 0)) : 0.0;
            $purchaseCost = 0.0;

            foreach ($details as $detail) {
                $itemId = (int) $detail['item_id'];
                if ($isDelivered && $itemId > 0) {
                    if (! array_key_exists($itemId, $purchaseCosts)) {
                        $missingPurchaseCosts[$itemId] = true;
                    }
                    $purchaseCost += ($purchaseCosts[$itemId] ?? 0) * $detail['quantity'];
                }
            }

            $profit = $deliveredRevenue - $purchaseCost - $deliveryCost - $codCharge - $returnCost;
            $campaignKey = $attribution->campaign_id ?: '__unknown__';
            $adsetKey = $attribution->adset_id ?: '__unknown__';
            $adKey = $attribution->ad_id ?: '__unknown__';
            $campaigns[$campaignKey] ??= $this->emptyLocal();
            $adsets[$adsetKey] ??= $this->emptyLocal();
            $ads[$adKey] ??= $this->emptyLocal();

            $this->addOrderMetrics(
                $summary,
                $orderValue,
                $deliveredRevenue,
                $returnedValue,
                $cancelledValue,
                $isDelivered,
                $isReturned,
                $profit
            );
            $this->addOrderMetrics(
                $campaigns[$campaignKey],
                $orderValue,
                $deliveredRevenue,
                $returnedValue,
                $cancelledValue,
                $isDelivered,
                $isReturned,
                $profit
            );
            $this->addOrderMetrics(
                $adsets[$adsetKey],
                $orderValue,
                $deliveredRevenue,
                $returnedValue,
                $cancelledValue,
                $isDelivered,
                $isReturned,
                $profit
            );
            $this->addOrderMetrics(
                $ads[$adKey],
                $orderValue,
                $deliveredRevenue,
                $returnedValue,
                $cancelledValue,
                $isDelivered,
                $isReturned,
                $profit
            );

            foreach ($details as $detail) {
                $productKey = (string) $detail['item_id'];
                if ($productKey === '0') {
                    continue;
                }

                $products[$productKey] ??= array_replace($this->emptyLocal(), [
                    'product_name' => $detail['item_name'],
                ]);
                $weight = $detail['weight'];
                $linePurchaseCost = $isDelivered
                    ? ($purchaseCosts[(int) $detail['item_id']] ?? 0) * $detail['quantity']
                    : 0.0;
                $lineProfit = ($deliveredRevenue * $weight)
                    - $linePurchaseCost
                    - (($deliveryCost + $codCharge + $returnCost) * $weight);
                $this->addOrderMetrics(
                    $products[$productKey],
                    $orderValue * $weight,
                    $deliveredRevenue * $weight,
                    $returnedValue * $weight,
                    $cancelledValue * $weight,
                    $isDelivered,
                    $isReturned,
                    $lineProfit
                );
            }
        }

        return [
            'summary' => $summary,
            'campaigns' => $campaigns,
            'adsets' => $adsets,
            'ads' => $ads,
            'products' => $products,
            'missing_purchase_cost_items' => count($missingPurchaseCosts),
        ];
    }

    private function addOrderMetrics(
        array &$bucket,
        float $orderValue,
        float $deliveredRevenue,
        float $returnedValue,
        float $cancelledValue,
        bool $isDelivered,
        bool $isReturned,
        float $profit
    ): void {
        $bucket['attributed_order_count']++;
        $bucket['attributed_order_value'] += $orderValue;
        $bucket['delivered_revenue'] += $deliveredRevenue;
        $bucket['returned_value'] += $returnedValue;
        $bucket['cancelled_value'] += $cancelledValue;
        $bucket['delivered_orders'] += $isDelivered ? 1 : 0;
        $bucket['returned_orders'] += $isReturned ? 1 : 0;
        $bucket['profit'] += $profit;
    }

    private function detailRows(Collection $details): array
    {
        $prepared = $details
            ->map(function (OrderDetail $detail) {
                $quantity = max(1, (int) $detail->display_quantity);
                $gross = max(0, (float) $detail->display_price * $quantity);
                $discount = max(0, (float) $detail->discount_on_item * $quantity);

                return [
                    'item_id' => (int) ($detail->display_item_reference_id ?? 0),
                    'item_name' => $detail->display_item_name,
                    'quantity' => $quantity,
                    'base' => max($gross - $discount, 0),
                ];
            })
            ->groupBy('item_id')
            ->map(function (Collection $rows) {
                return [
                    'item_id' => (int) $rows->first()['item_id'],
                    'item_name' => (string) $rows->first()['item_name'],
                    'quantity' => (int) $rows->sum('quantity'),
                    'base' => (float) $rows->sum('base'),
                ];
            })
            ->values();
        $baseTotal = (float) $prepared->sum('base');
        $quantityTotal = max(1, (int) $prepared->sum('quantity'));

        return $prepared->map(function (array $row) use ($baseTotal, $quantityTotal) {
            $row['weight'] = $baseTotal > 0
                ? $row['base'] / $baseTotal
                : $row['quantity'] / $quantityTotal;

            return $row;
        })->all();
    }

    private function deliveredRevenue(Order $order, string $status): float
    {
        if (str_contains($status, 'partial') && str_contains($status, 'deliver')) {
            $collected = data_get($order, 'courierShipment.cod_collected_amount');
            if ($collected !== null && is_numeric($collected)) {
                return max(0, (float) $collected);
            }

            $collectionStatus = $this->normalizeStatus((string) data_get($order, 'courierShipment.cod_collection_status'));
            $collectable = data_get($order, 'courierShipment.collectable_amount');
            if (in_array($collectionStatus, ['collected', 'cod_collected', 'cash_collected', 'paid', 'settled'], true)
                && $collectable !== null
                && is_numeric($collectable)
            ) {
                return max(0, (float) $collectable);
            }

            return 0.0;
        }

        return max(0, (float) $order->order_amount);
    }

    private function averagePurchaseCostMap(Carbon $to): array
    {
        if (! Schema::hasTable('purchase_items') || ! Schema::hasTable('purchases')) {
            return [];
        }

        return PurchaseItem::query()
            ->join('purchases', 'purchases.id', '=', 'purchase_items.purchase_id')
            ->whereDate('purchases.purchase_date', '<=', $to->toDateString())
            ->selectRaw('purchase_items.item_id, SUM(purchase_items.quantity) as total_quantity, SUM(purchase_items.total_amount) as total_amount')
            ->groupBy('purchase_items.item_id')
            ->get()
            ->filter(fn ($row) => (float) $row->total_quantity > 0)
            ->mapWithKeys(fn ($row) => [
                (int) $row->item_id => (float) $row->total_amount / (float) $row->total_quantity,
            ])
            ->all();
    }

    private function aggregateInsights(Collection $rows): array
    {
        return [
            'impressions' => (int) $rows->sum('impressions'),
            'clicks' => (int) $rows->sum('clicks'),
            'add_to_cart' => (float) $rows->sum('add_to_cart'),
            'purchases' => (float) $rows->sum('purchases'),
            'spend' => (float) $rows->sum('spend'),
            'purchase_value' => (float) $rows->sum('purchase_value'),
            'currency' => (string) ($rows->firstWhere('currency', '!=', null)?->currency ?: 'BDT'),
        ];
    }

    private function emptyInsight(): array
    {
        return [
            'impressions' => 0,
            'clicks' => 0,
            'add_to_cart' => 0.0,
            'purchases' => 0.0,
            'spend' => 0.0,
            'purchase_value' => 0.0,
            'currency' => 'BDT',
        ];
    }

    private function emptyLocal(): array
    {
        return [
            'attributed_order_count' => 0,
            'attributed_order_value' => 0.0,
            'delivered_orders' => 0,
            'delivered_revenue' => 0.0,
            'returned_orders' => 0,
            'returned_value' => 0.0,
            'cancelled_value' => 0.0,
            'profit' => 0.0,
        ];
    }

    private function statusSet(string $group): array
    {
        return collect($this->customerQualityService->statusesFor($group))
            ->map(fn ($status) => $this->normalizeStatus((string) $status))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function normalizeStatus(string $status): string
    {
        return trim(preg_replace('/[^a-z0-9]+/', '_', strtolower($status)) ?? '', '_');
    }

    private function totalOrders(Carbon $from, Carbon $to): int
    {
        if (! Schema::hasTable('orders')) {
            return 0;
        }

        return Order::query()
            ->whereBetween('created_at', [$from, $to])
            ->where('order_type', '!=', 'parcel')
            ->count();
    }

    private function dateRange(Request $request): array
    {
        try {
            $to = $request->filled('to')
                ? Carbon::parse((string) $request->query('to'))->endOfDay()
                : now()->endOfDay();
            $from = $request->filled('from')
                ? Carbon::parse((string) $request->query('from'))->startOfDay()
                : $to->copy()->subDays(29)->startOfDay();
        } catch (\Throwable) {
            $to = now()->endOfDay();
            $from = $to->copy()->subDays(29)->startOfDay();
        }

        if ($from->gt($to)) {
            [$from, $to] = [$to->copy()->startOfDay(), $from->copy()->endOfDay()];
        }

        if ($from->diffInDays($to) > 180) {
            $from = $to->copy()->subDays(179)->startOfDay();
        }

        return [$from, $to];
    }
}
