<?php

declare(strict_types=1);

namespace App\Services\Supplier;

use App\Exceptions\Supplier\SupplierAuthException;
use App\Exceptions\Supplier\SupplierClientException;
use App\Exceptions\Supplier\SupplierTransientException;
use App\Jobs\Supplier\RefreshSupplierSessionJob;
use App\Models\SupplierProject;
use App\Services\Supplier\Dto\SupplierProjectDto;
use Carbon\CarbonInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Factory as HttpFactory;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;

/**
 * HTTP-обёртка над rt-* AJAX endpoints supplier-портала crm.bp-gr.ru.
 *
 * Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §4.4
 *
 * Endpoints (placeholder, точные имена адаптируются после Task 1 discovery):
 * - GET  /admin/rt-projects-load — список проектов
 * - POST /admin/rt-project-save — создание
 * - POST /admin/rt-project-update — обновление
 * - POST /admin/rt-project-delete — удаление
 *
 * Авторизация: PHPSESSID cookie + X-CSRF-Token header (Redis cache 'supplier:session').
 * На 401/403 — single retry через dispatch_sync(RefreshSupplierSessionJob).
 */
class SupplierPortalClient
{
    public function __construct(
        private readonly HttpFactory $http,
    ) {}

    /**
     * Идемпотентно обеспечивает наличие supplier_project-записи для переданной
     * тройки (platform, signalType, uniqueKey). Если запись уже существует —
     * возвращает её id. Иначе — создаёт проект на стороне поставщика через
     * saveProject() и сохраняет новую запись supplier_projects.
     *
     * Используется SyncSupplierProjectJob (Plan 5 Task 4).
     *
     * В тестах метод мокируется через $this->mock(SupplierPortalClient::class) —
     * реальное тело не вызывается.
     *
     * @param  string  $platform  B1 / B2 / B3
     * @param  string  $signalType  site / call / sms
     * @param  string  $uniqueKey  domain / phone / sender+keyword / sender
     */
    public function ensureSupplierProject(string $platform, string $signalType, string $uniqueKey): int
    {
        $existing = SupplierProject::query()
            ->where('platform', $platform)
            ->where('signal_type', $signalType)
            ->where('unique_key', $uniqueKey)
            ->first();

        if ($existing !== null) {
            return $existing->id;
        }

        $dto = new SupplierProjectDto(
            platform: $platform,
            signalType: $signalType,
            uniqueKey: $uniqueKey,
            limit: 0,
            workdays: [1, 2, 3, 4, 5, 6, 7],
            regions: [],
            regionsReverse: false,
            status: 'active',
        );

        $externalId = $this->saveProject($dto);

        $sp = SupplierProject::query()->create([
            'platform' => $platform,
            'signal_type' => $signalType,
            'unique_key' => $uniqueKey,
            'supplier_external_id' => (string) $externalId,
            'current_limit' => 0,
            'current_workdays' => [1, 2, 3, 4, 5, 6, 7],
            'current_regions' => null,
            'sync_status' => 'ok',
        ]);

        return $sp->id;
    }

    /**
     * @return array<int, mixed>
     */
    public function listProjects(): array
    {
        $response = $this->request('GET', '/admin/rt-projects-load');

        return $response->json() ?? [];
    }

    public function saveProject(SupplierProjectDto $dto): int
    {
        $response = $this->request('POST', '/admin/rt-project-save', $this->toPayload($dto));

        return (int) ($response->json('id') ?? 0);
    }

    public function updateProject(int $externalId, SupplierProjectDto $dto): void
    {
        $this->request('POST', '/admin/rt-project-update', array_merge(
            ['id' => $externalId],
            $this->toPayload($dto)
        ));
    }

    public function deleteProject(int $externalId): void
    {
        $this->request('POST', '/admin/rt-project-delete', ['id' => $externalId]);
    }

    /**
     * GET /admin/report/index?type=49 — CSV-экспорт лидов за окно [from, to].
     * Auth/retry семантика наследуется от request() (PHPSESSID + X-CSRF-Token +
     * 401 → RefreshSession + 5xx → SupplierTransientException + 4xx → SupplierClientException).
     *
     * Возвращает raw CSV-body (UTF-8 + BOM, CRLF). Парсинг — снаружи через
     * SupplierCsvParser (streaming через generator).
     *
     * Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §5.1
     */
    public function downloadLeadsCsv(CarbonInterface $from, CarbonInterface $to): string
    {
        $response = $this->request('GET', '/admin/report/index', [
            'type' => 49,
            'from' => $from->format('Y-m-d H:i:s'),
            'to' => $to->format('Y-m-d H:i:s'),
        ]);

        return $response->body();
    }

    /**
     * @param  array<string, mixed>  $body
     */
    private function request(string $method, string $path, array $body = [], bool $isRetry = false): Response
    {
        $session = $this->loadSession();
        $portalUrl = (string) config('services.supplier.portal_url');
        $url = rtrim($portalUrl, '/').$path;
        $host = parse_url($url, PHP_URL_HOST);
        if (! is_string($host) || $host === '') {
            throw new SupplierClientException(
                "Misconfigured services.supplier.portal_url: cannot parse host from '{$url}'",
            );
        }

        $request = $this->http
            ->withCookies(['PHPSESSID' => $session['phpsessid']], $host)
            ->withHeaders(['X-CSRF-Token' => $session['csrf']])
            ->timeout(30);

        try {
            if ($method === 'GET') {
                $response = $request->get($url, $body);
            } else {
                $response = $request->asForm()->post($url, $body);
            }
        } catch (ConnectionException $e) {
            throw new SupplierTransientException(
                "Network error: {$e->getMessage()}",
                previous: $e,
            );
        }

        if ($response->status() === 401 || $response->status() === 403) {
            if ($isRetry) {
                throw new SupplierAuthException(
                    "Sticky auth failure on {$path}",
                    httpStatus: $response->status(),
                    responseBody: $response->body(),
                );
            }
            try {
                dispatch_sync(app(RefreshSupplierSessionJob::class));
            } catch (\Throwable $e) {
                throw new SupplierAuthException(
                    "Session refresh failed during retry on {$path}: {$e->getMessage()}",
                    httpStatus: $response->status(),
                    previous: $e,
                );
            }

            return $this->request($method, $path, $body, isRetry: true);
        }

        if ($response->status() >= 500) {
            throw new SupplierTransientException(
                "Supplier server error {$response->status()} on {$path}",
                httpStatus: $response->status(),
                responseBody: $response->body(),
            );
        }

        if ($response->status() >= 400) {
            throw new SupplierClientException(
                "Supplier rejected {$path}: HTTP {$response->status()}",
                httpStatus: $response->status(),
                responseBody: $response->body(),
            );
        }

        return $response;
    }

    /**
     * @return array{phpsessid: string, csrf: string, refreshed_at?: string}
     */
    private function loadSession(): array
    {
        /** @var array{phpsessid?: string, csrf?: string, refreshed_at?: string}|null $session */
        $session = Cache::store('redis')->get('supplier:session');

        if ($session === null || ! isset($session['phpsessid'], $session['csrf'])) {
            try {
                dispatch_sync(app(RefreshSupplierSessionJob::class));
            } catch (\Throwable $e) {
                throw new SupplierAuthException(
                    "Session refresh failed during loadSession: {$e->getMessage()}",
                    previous: $e,
                );
            }
            /** @var array{phpsessid?: string, csrf?: string, refreshed_at?: string}|null $session */
            $session = Cache::store('redis')->get('supplier:session');

            if ($session === null || ! isset($session['phpsessid'], $session['csrf'])) {
                throw new SupplierAuthException('Session refresh failed during loadSession: cache still empty');
            }
        }

        /** @var array{phpsessid: string, csrf: string, refreshed_at?: string} $session */
        return $session;
    }

    /**
     * NOTE: payload-shape — placeholder. Точные поля будут адаптированы
     * после Task 1 discovery + Task 2 spec §4.4 (отдельный fixup commit
     * перед Task 6 при расхождении).
     *
     * @return array<string, mixed>
     */
    private function toPayload(SupplierProjectDto $dto): array
    {
        return [
            'platform' => $dto->platform,
            'signal_type' => $dto->signalType,
            'unique_key' => $dto->uniqueKey,
            'limit' => $dto->limit,
            'workdays' => $dto->workdays,
            'regions' => $dto->regions,
            'regions_reverse' => $dto->regionsReverse ? 1 : 0,
            'status' => $dto->status,
        ];
    }
}
