<?php

namespace App\Support;

use App\Models\Offerwall;
use App\Models\ProviderOffer;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Throwable;

class ProviderOfferImporter
{
    public function enabled(Offerwall $offerwall): bool
    {
        return $offerwall->apiFeedEnabled();
    }

    public function configured(Offerwall $offerwall): bool
    {
        return $this->feedUrl($this->configFor($offerwall)) !== '';
    }

    public function testConnection(array $config): array
    {
        $feedUrl = $this->feedUrl($config);

        if ($feedUrl === '') {
            return ['success' => false, 'count' => 0, 'message' => $this->missingFeedMessage($config)];
        }

        try {
            OutboundUrl::assertPublicHttp($feedUrl);
            $method = strtoupper($config['offer_feed_method'] ?? 'GET');
            $response = Http::timeout(20)
                ->retry(2, 250)
                ->withOptions(['allow_redirects' => false])
                ->withHeaders($this->headers($config))
                ->acceptJson()
                ->send($method, $feedUrl, $this->requestOptions($config, $method));
        } catch (Throwable $exception) {
            return ['success' => false, 'count' => 0, 'message' => 'Connection failed: '.$exception->getMessage()];
        }

        if (!$response->successful()) {
            return ['success' => false, 'count' => 0, 'message' => 'API returned HTTP '.$response->status().'.'];
        }

        $payload = $response->json();
        if (!is_array($payload)) {
            return ['success' => false, 'count' => 0, 'message' => 'API did not return valid JSON.'];
        }
        if (($payload['success'] ?? true) === false) {
            return ['success' => false, 'count' => 0, 'message' => (string) ($payload['message'] ?? 'Provider API reported an error.')];
        }

        $items = $this->items($payload, (string) ($config['offer_feed_root'] ?? ''));

        return ['success' => true, 'count' => count($items), 'message' => 'Connection successful.'];
    }

    public function sync(Offerwall $offerwall): array
    {
        if (!$offerwall->is_active) {
            return $this->failedResult($offerwall, [
                'success' => false,
                'imported' => 0,
                'updated' => 0,
                'skipped' => 0,
                'received' => 0,
                'message' => 'Provider is disabled.',
            ]);
        }

        if (!$this->enabled($offerwall)) {
            return $this->failedResult($offerwall, [
                'success' => false,
                'imported' => 0,
                'updated' => 0,
                'skipped' => 0,
                'received' => 0,
                'message' => 'API feed is disabled.',
            ]);
        }

        $config = $this->configFor($offerwall);
        $feedUrl = $this->feedUrl($config);

        if ($feedUrl === '') {
            return $this->failedResult($offerwall, ['success' => false, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'received' => 0, 'message' => $this->missingFeedMessage($config)]);
        }

        try {
            OutboundUrl::assertPublicHttp($feedUrl);
            $method = strtoupper($config['offer_feed_method'] ?? 'GET');
            $requestOptions = $this->requestOptions($config, $method);

            $response = Http::timeout(20)
                ->retry(2, 250)
                ->withOptions(['allow_redirects' => false])
                ->withHeaders($this->headers($config))
                ->acceptJson()
                ->send($method, $feedUrl, $requestOptions);
        } catch (Throwable $exception) {
            return $this->failedResult($offerwall, ['success' => false, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'received' => 0, 'message' => 'Provider feed connection failed: '.$exception->getMessage()]);
        }

        if (!$response->successful()) {
            return $this->failedResult($offerwall, ['success' => false, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'received' => 0, 'message' => 'Provider feed returned HTTP '.$response->status().'.']);
        }

        $payload = $response->json();

        if (!is_array($payload)) {
            return $this->failedResult($offerwall, ['success' => false, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'received' => 0, 'message' => 'Provider feed did not return valid JSON.']);
        }

        if (is_array($payload) && array_key_exists('success', $payload) && $payload['success'] === false) {
            return $this->failedResult($offerwall, ['success' => false, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'received' => 0, 'message' => (string) ($payload['message'] ?? 'Provider feed reported an error.')]);
        }

        $items = $this->items($payload, (string) ($config['offer_feed_root'] ?? ''));

        $imported = 0;
        $updated = 0;
        $skipped = 0;
        $seen = [];

        foreach ($items as $item) {
            if (!is_array($item)) {
                $skipped++;
                continue;
            }

            $externalId = (string) $this->pick($item, $config, 'offer_id', ['id', 'offer_id', 'campaign_id', 'campaignId', 'offerId']);
            $name = trim((string) $this->pick($item, $config, 'name', ['name', 'title', 'offer_name', 'offerName', 'campaign_name']));

            if ($name === '') {
                $skipped++;
                continue;
            }

            if ($externalId === '') {
                $externalId = Str::slug($offerwall->name.'-'.$name);
            }

            $seen[] = $externalId;
            $directUrl = (string) $this->pick($item, $config, 'url', ['url', 'link', 'click_url', 'clickUrl', 'tracking_url', 'trackingUrl', 'offer_url', 'offerUrl']);
            $reward = $this->number($this->pick($item, $config, 'reward', ['reward', 'rewards.0.amount', 'points', 'coins', 'payout_points', 'payoutPoints']));
            $payout = $this->number($this->pick($item, $config, 'payout', ['payout', 'payout_usd', 'payoutUsd', 'revenue', 'amount']));

            if ($reward <= 0 && $payout > 0) {
                $reward = $payout * coins_per_usd();
            }

            $category = strtolower((string) $this->pick($item, $config, 'category', ['category', 'type', 'vertical']));
            $devices = $this->arrayValue($this->pick($item, $config, 'devices', ['devices', 'device', 'platforms', 'platform']));
            $events = $this->arrayValue($this->pick($item, $config, 'events', ['events']));
            $steps = $this->normalizeSteps($this->pick($item, $config, 'steps', ['steps', 'goals', 'milestones']), $events);
            $rewards = $this->normalizeRewards($this->pick($item, $config, 'rewards', ['rewards', 'goals', 'milestones']), $events, $reward);
            $description = $this->localizedText($this->pick($item, $config, 'description', ['description', 'desc', 'subtitle', 'short_description']));
            $requirements = $this->localizedText($this->pick($item, $config, 'requirements', ['requirements', 'instructions', 'conversion', 'conversion_point']));

            if ($requirements === '' && $steps) {
                $requirements = $steps[0];
            }

            $offer = ProviderOffer::withTrashed()->firstOrNew([
                'offerwall_id' => $offerwall->id,
                'provider_offer_id' => $externalId,
            ]);

            // A deleted offer stays deleted and a hidden offer stays hidden.
            // Provider refreshes update attributes without undoing admin choices.
            if ($offer->trashed()) {
                $skipped++;
                continue;
            }

            $isNew = !$offer->exists;
            $offer->fill([
                    'name' => $name,
                    'description' => $description,
                    'requirements' => $requirements,
                    'steps' => $steps ?: null,
                    'rewards' => $rewards ?: null,
                    'reward_points' => (int) round($reward),
                    'payout' => $payout,
                    'icon_url' => (string) $this->pick($item, $config, 'icon', ['icon', 'icon_url', 'iconUrl', 'image', 'image_url', 'imageUrl', 'thumbnail', 'picture']),
                    'direct_url' => $directUrl,
                    'launch_mode' => $config['offer_launch_mode'] ?? $offerwall->launch_mode ?? 'iframe',
                    'category' => $this->normalizeCategory($category, $offerwall, $devices),
                    'devices' => $devices ?: ['android', 'ios', 'desktop'],
                    'countries' => is_array($countries = $this->pick($item, $config, 'countries', ['countries', 'available_in', 'geo', 'geos', 'country', 'country_code', 'countryCode'])) ? implode(',', $countries) : (string) $countries,
                    'last_synced_at' => now(),
                    'raw_payload' => $item,
            ]);

            if ($isNew) {
                $offer->is_active = true;
            }

            $offer->save();

            $isNew ? $imported++ : $updated++;
        }

        // The API is the source of truth. Remove cards absent from a successful
        // response while retaining soft-deleted admin tombstones.
        $hideMissing = filter_var($config['offer_feed_hide_missing'] ?? true, FILTER_VALIDATE_BOOLEAN);
        $deleted = 0;

        if ($hideMissing) {
            $missing = ProviderOffer::where('offerwall_id', $offerwall->id)
                ->whereNull('deleted_at');
            if ($seen) {
                $missing->whereNotIn('provider_offer_id', array_unique($seen));
            }
            $deleted = $missing->forceDelete();
        }

        return $this->recordResult($offerwall, [
            'success' => true,
            'imported' => $imported,
            'updated' => $updated,
            'skipped' => $skipped,
            'deleted' => $deleted,
            'received' => count($items),
            'message' => "Synced {$imported} new, {$updated} updated, {$deleted} removed, {$skipped} skipped.",
        ]);
    }

    public function clearStoredOffers(Offerwall $offerwall): int
    {
        // Deliberately soft-deleted records are admin suppression tombstones.
        // Only live imported rows are purged when an API is unavailable.
        return ProviderOffer::where('offerwall_id', $offerwall->id)
            ->whereNull('deleted_at')
            ->forceDelete();
    }

    private function failedResult(Offerwall $offerwall, array $result): array
    {
        $deleted = $this->clearStoredOffers($offerwall);
        $result['deleted'] = $deleted;
        $result['message'] = rtrim((string) ($result['message'] ?? ''), '.').". Cleared {$deleted} stale offer(s).";

        return $this->recordResult($offerwall, $result);
    }

    private function recordResult(Offerwall $offerwall, array $result): array
    {
        $config = $offerwall->sdk_data ?: [];
        $config['offer_feed_last_checked_at'] = now()->toIso8601String();
        $config['offer_feed_last_success'] = (bool) ($result['success'] ?? false);
        $config['offer_feed_last_message'] = (string) ($result['message'] ?? '');
        $config['offer_feed_last_count'] = (int) ($result['received'] ?? 0);
        $offerwall->forceFill(['sdk_data' => $config])->save();

        return $result;
    }

    private function feedUrl(array $config): string
    {
        if ($this->isGemiWallFeed($config)) {
            $placementId = trim((string) ($config['gemiwall_placement_id'] ?? ''));
            $apiKey = trim((string) ($config['gemiwall_api_key'] ?? $config['api_key'] ?? $config['postback_secret'] ?? ''));

            if ($placementId === '' || $apiKey === '') {
                return '';
            }

            return 'http://api.gemiwall.com/api/offers/static?'.http_build_query([
                'placementId' => $placementId,
                'apiKey' => $apiKey,
            ]);
        }

        return trim((string) ($config['offer_feed_url'] ?? ''));
    }

    private function configFor(Offerwall $offerwall): array
    {
        $config = $offerwall->sdk_data ?: [];

        if (trim((string) ($config['offer_feed_url'] ?? '')) !== '') {
            return $config;
        }

        // UpWall exposes its app_id in the iframe URL and uses the same value
        // for its documented server-side offers endpoint, so this legacy
        // configuration can be upgraded without inventing a credential.
        if (str_contains(strtolower($offerwall->name), 'upwall')) {
            parse_str((string) parse_url($offerwall->iframe_url, PHP_URL_QUERY), $iframeQuery);
            $appId = trim((string) ($iframeQuery['app_id'] ?? ''));

            if ($appId !== '') {
                $config['offer_feed_url'] = 'https://offerwall.upwall.io/api/';
                $config['offer_feed_root'] = 'offers';
                $config['offer_feed_method'] = 'GET';
                $config['offer_feed_query_params'] = [
                    'app_id' => $appId,
                    'userid' => '{user_id}',
                    'os' => 'web',
                ];
                $config['offer_feed_hide_missing'] = true;
            }
        }

        return $config;
    }

    private function requestOptions(array $config, string $method): array
    {
        $queryParams = $this->params($config['offer_feed_query_params'] ?? []);
        $bodyParams = $this->params($config['offer_feed_body_params'] ?? []);

        if ($this->isGemiWallFeed($config)) {
            return [];
        }

        $options = [];

        if ($queryParams) {
            $options['query'] = $queryParams;
        }

        if ($method !== 'GET' && $bodyParams) {
            $bodyType = strtolower((string) ($config['offer_feed_body_type'] ?? 'json'));
            $options[$bodyType === 'form' ? 'form_params' : 'json'] = $bodyParams;
        }

        return $options;
    }

    private function params($raw): array
    {
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            $raw = is_array($decoded) ? $decoded : [];
        }

        if (!is_array($raw)) {
            return [];
        }

        $params = [];

        foreach ($raw as $key => $value) {
            if (is_array($value) && array_key_exists('key', $value)) {
                $key = $value['key'];
                $value = $value['value'] ?? '';
            }

            $key = trim((string) $key);

            if ($key === '') {
                continue;
            }

            $params[$key] = $this->replaceConfigPlaceholders((string) $value);
        }

        return $params;
    }

    private function replaceConfigPlaceholders(string $value): string
    {
        return str_replace(
            ['{today}', '{date}', '{timestamp}', '{country}', '{country_code}', '{os}', '{user_id}', '{userid}', '{uniqueUserID}'],
            [now()->toDateString(), now()->toDateString(), (string) now()->timestamp, request_country_code() ?: '', request_country_code() ?: '', 'web', '0', '0', '0'],
            $value
        );
    }

    private function missingFeedMessage(array $config): string
    {
        return $this->isGemiWallFeed($config)
            ? 'Missing GemiAd/GemiWall placement ID or API key.'
            : 'No offer feed URL configured.';
    }

    private function isGemiWallFeed(array $config): bool
    {
        $provider = (string) ($config['offer_feed_provider'] ?? 'generic');
        $feedUrl = strtolower((string) ($config['offer_feed_url'] ?? ''));

        return $provider === 'gemiwall'
            || (trim((string) ($config['gemiwall_placement_id'] ?? '')) !== '' && str_contains($feedUrl, 'gemiwall'));
    }

    private function headers(array $config): array
    {
        $headers = [];
        $raw = trim((string) ($config['offer_feed_headers'] ?? ''));

        if ($raw === '') {
            return $headers;
        }

        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            return $decoded;
        }

        foreach (explode("\n", $raw) as $line) {
            if (!str_contains($line, ':')) {
                continue;
            }

            [$key, $value] = explode(':', $line, 2);
            $headers[trim($key)] = trim($value);
        }

        return $headers;
    }

    private function items($payload, string $root): array
    {
        $items = $root !== '' ? data_get($payload, $root, []) : $payload;

        if (isset($items['data']) && is_array($items['data'])) {
            $items = $items['data'];
        }

        if (isset($items['offers']) && is_array($items['offers'])) {
            $items = $items['offers'];
        }

        if (isset($items['results']) && is_array($items['results'])) {
            $items = $items['results'];
        }

        if (isset($items['data']['offers']) && is_array($items['data']['offers'])) {
            $items = $items['data']['offers'];
        }

        return is_array($items) ? array_values($items) : [];
    }

    private function pick(array $item, array $config, string $key, array $fallbacks)
    {
        $configured = trim((string) ($config['offer_field_'.$key] ?? ''));
        $paths = array_filter(array_merge([$configured], $fallbacks));

        foreach ($paths as $path) {
            $value = data_get($item, $path);
            if ($value !== null && $value !== '') {
                return $value;
            }
        }

        return null;
    }

    private function number($value): float
    {
        if (is_numeric($value)) {
            return (float) $value;
        }

        return (float) preg_replace('/[^0-9.]/', '', (string) $value);
    }

    private function arrayValue($value): array
    {
        if (is_array($value)) {
            return array_values($value);
        }

        if (is_string($value) && trim($value) !== '') {
            return collect(preg_split('/[,|]/', $value))
                ->map(fn ($item) => trim($item))
                ->filter()
                ->values()
                ->all();
        }

        return [];
    }

    private function localizedText($value): string
    {
        if (is_array($value)) {
            foreach (['en', 'EN', 'default', 'text', 'label', 'name', 'description', 'action'] as $key) {
                if (isset($value[$key]) && !is_array($value[$key])) {
                    return trim((string) $value[$key]);
                }
            }

            foreach ($value as $candidate) {
                if (!is_array($candidate) && trim((string) $candidate) !== '') {
                    return trim((string) $candidate);
                }
            }

            return '';
        }

        return trim((string) $value);
    }

    private function normalizeSteps($value, array $events): array
    {
        $steps = collect($this->arrayValue($value))
            ->map(function ($step) {
                if (is_array($step)) {
                    return $this->localizedText($step['action'] ?? $step['description'] ?? $step['label'] ?? $step['name'] ?? $step);
                }

                return $this->localizedText($step);
            })
            ->map(fn ($step) => trim((string) $step))
            ->reject(fn ($step) => \App\Support\DirectOfferFeed::isGarbageMilestoneLabel($step))
            ->values();

        if ($steps->isNotEmpty()) {
            return $steps->all();
        }

        return collect($events)
            ->map(fn ($event) => is_array($event) ? $this->localizedText($event['action'] ?? $event['event_instructions'] ?? $event) : $this->localizedText($event))
            ->map(fn ($step) => trim((string) $step))
            ->reject(fn ($step) => \App\Support\DirectOfferFeed::isGarbageMilestoneLabel($step))
            ->values()
            ->all();
    }

    private function normalizeRewards($value, array $events, float $fallbackReward): array
    {
        $rewards = collect($this->arrayValue($value))
            ->map(function ($rewardRow) use ($fallbackReward) {
                if (is_array($rewardRow)) {
                    $coins = $this->number($rewardRow['coins'] ?? $rewardRow['points'] ?? $rewardRow['amount'] ?? $rewardRow['payout'] ?? $rewardRow['event_payout'] ?? $fallbackReward);

                    return [
                        'coins' => (int) max(1, round($coins)),
                        'label' => $this->localizedText($rewardRow['label'] ?? $rewardRow['description'] ?? $rewardRow['event_instructions'] ?? $rewardRow['action'] ?? 'Complete milestone.'),
                    ];
                }

                return [
                    'coins' => (int) max(1, round($fallbackReward)),
                    'label' => $this->localizedText($rewardRow),
                ];
            })
            ->filter(fn ($rewardRow) => ($rewardRow['label'] ?? '') !== '')
            ->values();

        if ($rewards->isNotEmpty()) {
            return $rewards->all();
        }

        return collect($events)
            ->map(function ($event) use ($fallbackReward) {
                if (!is_array($event)) {
                    return null;
                }

                $eventPayout = $this->number($event['payout'] ?? $event['event_payout'] ?? 0);
                $coins = $eventPayout > 0 ? $eventPayout * coins_per_usd() : $fallbackReward;

                return [
                    'coins' => (int) max(1, round($coins)),
                    'label' => $this->localizedText($event['action'] ?? $event['event_instructions'] ?? 'Complete milestone.'),
                ];
            })
            ->filter()
            ->values()
            ->all();
    }

    private function normalizeCategory(string $category, Offerwall $offerwall, array $devices): string
    {
        return match ($category) {
            'game', 'games', 'app' => 'games',
            'survey', 'surveys' => 'surveys',
            'partner', 'partners' => 'partners',
            'web', 'click', 'other' => 'other',
            default => $this->categoryFor($offerwall, $devices),
        };
    }

    private function categoryFor(Offerwall $offerwall, array $devices): string
    {
        if (($offerwall->type ?? 'offer') === 'survey') {
            return 'surveys';
        }

        return collect($devices)->contains(fn ($device) => in_array(strtolower((string) $device), ['android', 'ios', 'mobile'], true))
            ? 'games'
            : 'other';
    }
}
