<?php

namespace App\Services;

use App\Enums\ForexImportStagingStatus;
use App\Enums\InvestmentStatus;
use App\Enums\UserKycStatus;
use App\Models\ForexImportStagingItem;
use App\Models\Investment;
use App\Models\InvestmentSetting;
use App\Models\User;
use App\Models\UserWallet;
use App\Support\InvestmentLedger;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use RuntimeException;

class ForexCsvImportService
{
    public const IMPORT_TXID_PREFIX = 'IMP-FX-';

    /**
     * @return array{
     *     anchors_seen: int,
     *     continuation_rows: int,
     *     orphan_skipped: int,
     *     plans_queued_for_review: int,
     *     would_create_users: int,
     *     plans_parse_skipped: int,
     *     plans_duplicate_skipped: int,
     *     dry_run: bool,
     * }
     */
    public function importFromPath(string $path, bool $dryRun = false): array
    {
        if (! is_readable($path)) {
            throw new RuntimeException("No se puede leer el archivo: {$path}");
        }

        $handle = fopen($path, 'r');
        if ($handle === false) {
            throw new RuntimeException("No se pudo abrir el archivo: {$path}");
        }

        try {
            $headerRow = fgetcsv($handle, 0, ',', '"', '\\');
            if ($headerRow === false) {
                throw new RuntimeException('El CSV está vacío.');
            }

            $columnKeys = self::buildColumnKeyMap($headerRow);

            $stats = [
                'anchors_seen' => 0,
                'continuation_rows' => 0,
                'orphan_skipped' => 0,
                'plans_queued_for_review' => 0,
                'would_create_users' => 0,
                'plans_parse_skipped' => 0,
                'plans_duplicate_skipped' => 0,
                'dry_run' => $dryRun,
            ];

            $dryRunEmailsQueued = [];

            /** @var array<string, string> $person */
            $person = [];

            $recordIndex = 0;
            $monthlySnapshot = (float) InvestmentSetting::current()->monthly_return;

            while (($cells = fgetcsv($handle, 0, ',', '"', '\\')) !== false) {
                if ($cells === [null] || self::rowIsEmpty($cells)) {
                    continue;
                }

                $row = self::rowToAssoc($columnKeys, $cells);
                $nombre = trim((string) ($row['nombre'] ?? ''));

                if ($nombre !== '') {
                    $stats['anchors_seen']++;
                    $person = self::mergePersonFromRow($person, $row);
                } else {
                    $stats['continuation_rows']++;
                    if ($person === []) {
                        $stats['orphan_skipped']++;

                        continue;
                    }
                }

                $parsed = self::parsePlanRow($row, $person);
                if ($parsed === null) {
                    $stats['plans_parse_skipped']++;

                    continue;
                }

                $recordIndex++;
                $email = self::normalizeEmail($person['email'] ?? '');
                if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
                    $stats['plans_parse_skipped']++;

                    continue;
                }

                $checksum = self::computeStagingRowChecksum(
                    $email,
                    $recordIndex,
                    $parsed['amount'],
                    $parsed['duration_months'],
                    $parsed['confirmed_at'],
                    $parsed['maturity_at'],
                );

                $txid = self::IMPORT_TXID_PREFIX.substr($checksum, 0, 24);

                if (Investment::query()->where('txid', $txid)->exists()
                    || ForexImportStagingItem::query()->where('row_checksum', $checksum)->exists()) {
                    $stats['plans_duplicate_skipped']++;

                    continue;
                }

                if ($dryRun) {
                    $dryRunEmailsQueued[$email] = true;
                    $stats['plans_queued_for_review']++;

                    continue;
                }

                ForexImportStagingItem::query()->create([
                    'status' => ForexImportStagingStatus::Pending,
                    'row_checksum' => $checksum,
                    'csv_record_index' => $recordIndex,
                    'email_hint' => $email,
                    'first_name' => $person['first_name'] ?? null,
                    'last_name' => $person['last_name'] ?? null,
                    'contact_number' => $person['contact_number'] ?? null,
                    'country' => self::normalizeCountryCodeForUser($person['country'] ?? null),
                    'wallet_address' => $person['wallet_address'] ?? null,
                    'amount' => $parsed['amount'],
                    'duration_months' => $parsed['duration_months'],
                    'confirmed_at' => $parsed['confirmed_at'],
                    'maturity_at' => $parsed['maturity_at'],
                    'monthly_return_snapshot' => $monthlySnapshot,
                    'payload' => [
                        'row' => $row,
                        'person_snapshot' => $person,
                        'csv_saldo_ganancias' => $row['saldo_ganancias'] ?? null,
                        'csv_saldo_total' => $row['saldo_total'] ?? null,
                        'csv_saldo_bloqueado' => $row['saldo_bloqueado'] ?? null,
                        'csv_saldo_pagado' => $row['saldo_pagado'] ?? null,
                    ],
                ]);
                $stats['plans_queued_for_review']++;
            }

            foreach (array_keys($dryRunEmailsQueued) as $queuedEmail) {
                if (User::query()->where('email', $queuedEmail)->doesntExist()) {
                    $stats['would_create_users']++;
                }
            }

            return $stats;
        } finally {
            fclose($handle);
        }
    }

    /**
     * Crea usuario (si no existe) e inversiones activas con depósito inicial para todo el grupo pendiente del correo.
     *
     * @return int Número de inversiones creadas
     */
    public function approveStagingGroupByEmail(string $email, User $adminUser): int
    {
        $email = self::normalizeEmail($email);

        /** @var Collection<int, ForexImportStagingItem> $items */
        $items = ForexImportStagingItem::query()
            ->where('status', ForexImportStagingStatus::Pending)
            ->whereRaw('LOWER(email_hint) = ?', [$email])
            ->orderBy('csv_record_index')
            ->orderBy('id')
            ->get();

        if ($items->isEmpty()) {
            throw new RuntimeException('No hay registros pendientes de importación para este correo.');
        }

        return (int) DB::transaction(function () use ($items, $email, $adminUser): int {
            $person = self::personSnapshotForStagingGroup($items);
            $person['email'] = $email;

            $userResult = $this->firstOrCreateForexInvestor($email, $person);
            $user = $userResult['user'];

            $count = 0;
            foreach ($items as $item) {
                if ($item->status !== ForexImportStagingStatus::Pending) {
                    continue;
                }

                $investment = $this->createActiveForexInvestmentFromStagingItem($user, $item, $adminUser->id);
                $item->update([
                    'status' => ForexImportStagingStatus::Imported,
                    'investment_id' => $investment->id,
                ]);
                $count++;
            }

            return $count;
        });
    }

    /**
     * @param  Collection<int, ForexImportStagingItem>  $items
     * @return array<string, string>
     */
    public static function personSnapshotForStagingGroup(Collection $items): array
    {
        /** @var ForexImportStagingItem $first */
        $first = $items->sortBy(['csv_record_index', 'id'])->firstOrFail();

        $walletItem = $items->first(static function (ForexImportStagingItem $i): bool {
            return $i->wallet_address !== null && trim((string) $i->wallet_address) !== '';
        });

        return [
            'first_name' => (string) ($first->first_name ?? ''),
            'last_name' => (string) ($first->last_name ?? ''),
            'contact_number' => (string) ($first->contact_number ?? ''),
            'country' => self::normalizeCountryCodeForUser($first->country ?? null),
            'wallet_address' => (string) ($walletItem?->wallet_address ?? ''),
        ];
    }

    public function createActiveForexInvestmentFromStagingItem(
        User $user,
        ForexImportStagingItem $item,
        int $confirmedByUserId,
    ): Investment {
        $checksum = $item->row_checksum;
        $txid = self::IMPORT_TXID_PREFIX.substr($checksum, 0, 24);

        if (Investment::query()->where('txid', $txid)->exists()) {
            throw new RuntimeException('La inversión con este TXID ya existe: '.$txid);
        }

        return DB::transaction(function () use ($user, $item, $confirmedByUserId, $txid): Investment {
            $confirmedAt = $item->confirmed_at ?? Carbon::now();
            $maturityAt = $item->maturity_at ?? $confirmedAt->copy()->addMonths((int) $item->duration_months);

            $investment = Investment::query()->create([
                'user_id' => $user->id,
                'amount' => round((float) $item->amount, 2),
                'duration_months' => (int) $item->duration_months,
                'monthly_return_snapshot' => (float) $item->monthly_return_snapshot,
                'txid' => $txid,
                'status' => InvestmentStatus::Active,
                'confirmed_at' => $confirmedAt,
                'maturity_at' => $maturityAt,
                'confirmed_by' => $confirmedByUserId,
            ]);

            InvestmentLedger::recordInitialDepositIfMissing($investment->fresh());
            InvestmentLedger::creditReferralBonusesOnInvestmentApproval($investment->fresh());

            InvestmentLedger::catchUpForexImportAccruals($investment->fresh());
            InvestmentLedger::syncInvestmentContractProgress($investment->fresh());

            $addr = $item->wallet_address;
            if ($addr !== null && trim((string) $addr) !== '') {
                $this->ensureWallet($user, (string) $addr);
            }

            /** @var Investment $fresh */
            $fresh = $investment->fresh();

            return $fresh;
        });
    }

    /**
     * @param  array<string, string>  $person
     * @return array{user: User, created: bool}
     */
    private function firstOrCreateForexInvestor(string $email, array $person): array
    {
        $existing = User::query()->where('email', $email)->first();
        if ($existing !== null) {
            return ['user' => $existing, 'created' => false];
        }

        $firstName = trim((string) ($person['first_name'] ?? ''));
        if ($firstName === '') {
            $firstName = 'Importado';
        }

        $lastName = trim((string) ($person['last_name'] ?? ''));
        if ($lastName === '') {
            $lastName = 'FOREX';
        }

        $contact = trim((string) ($person['contact_number'] ?? ''));
        if ($contact === '') {
            $contact = 'N/D';
        }

        $country = self::normalizeCountryCodeForUser($person['country'] ?? null);

        $user = User::query()->create([
            'first_name' => $firstName,
            'last_name' => $lastName,
            'email' => $email,
            'contact_number' => $contact,
            'country' => $country,
            'password' => Hash::make(Str::password(32)),
            'is_admin' => false,
            'kyc_status' => UserKycStatus::Approved,
            'kyc_rejection_reason' => null,
            'referral_program_enabled' => false,
            'referred_by_user_id' => null,
        ]);

        $user->forceFill(['email_verified_at' => now()])->save();

        return ['user' => $user, 'created' => true];
    }

    private function ensureWallet(User $user, string $address): void
    {
        $address = trim($address);
        if ($address === '') {
            return;
        }

        $network = (string) InvestmentSetting::current()->payment_method;

        UserWallet::query()->firstOrCreate(
            [
                'user_id' => $user->id,
                'address' => $address,
            ],
            [
                'currency_network' => $network,
                'label' => 'Importación',
                'is_default' => false,
            ],
        );
    }

    /**
     * Normaliza etiqueta de cabecera: Excel/Latin-1 usa bytes ISO-8859-1 (p. ej. ó) que no coinciden con «duración» en UTF-8.
     */
    private static function normalizeHeaderLabel(string $label): string
    {
        $label = trim($label);
        if ($label === '') {
            return '';
        }

        if (! mb_check_encoding($label, 'UTF-8')) {
            $label = mb_convert_encoding($label, 'UTF-8', 'ISO-8859-1');
        }

        return mb_strtolower($label);
    }

    /**
     * Celdas de datos en Latin-1 (p. ej. «México») para persistir bien en UTF-8.
     */
    private static function normalizeCsvCell(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }

        if (! mb_check_encoding($value, 'UTF-8')) {
            return trim(mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'));
        }

        return $value;
    }

    /**
     * Clave comparable para mapear cabeceras aunque vengan con acentos (p. ej. «País», «Teléfono»).
     */
    private static function headerMatchKeyFromLabel(string $label): string
    {
        return mb_strtolower(Str::ascii(self::normalizeHeaderLabel($label)));
    }

    /**
     * Convierte texto del CSV (código ISO, nombre del país, etc.) al código que usa {@see User::$country}.
     */
    public static function normalizeCountryCodeForUser(?string $raw): string
    {
        $raw = trim((string) $raw);
        if ($raw === '') {
            return 'OTHER';
        }

        $upper = mb_strtoupper($raw);
        if (strlen($upper) === 2 && in_array($upper, self::allowedInvestorCountryCodes(), true)) {
            return $upper;
        }

        $fold = mb_strtolower(Str::ascii($raw));

        /** @var array<string, string> */
        static $aliases = [
            'argentina' => 'AR',
            'bolivia' => 'BO',
            'brasil' => 'BR',
            'brazil' => 'BR',
            'chile' => 'CL',
            'colombia' => 'CO',
            'costa rica' => 'CR',
            'cuba' => 'CU',
            'republica dominicana' => 'DO',
            'dominican republic' => 'DO',
            'ecuador' => 'EC',
            'el salvador' => 'SV',
            'guatemala' => 'GT',
            'honduras' => 'HN',
            'mexico' => 'MX',
            'nicaragua' => 'NI',
            'panama' => 'PA',
            'paraguay' => 'PY',
            'peru' => 'PE',
            'espana' => 'ES',
            'spain' => 'ES',
            'uruguay' => 'UY',
            'venezuela' => 'VE',
            'estados unidos' => 'US',
            'united states' => 'US',
            'usa' => 'US',
            'otro' => 'OTHER',
            'other' => 'OTHER',
        ];

        return $aliases[$fold] ?? 'OTHER';
    }

    /**
     * @return array<int, string>
     */
    private static function allowedInvestorCountryCodes(): array
    {
        return [
            'AR', 'BO', 'BR', 'CL', 'CO', 'CR', 'CU', 'DO', 'EC', 'SV', 'GT', 'HN',
            'MX', 'NI', 'PA', 'PY', 'PE', 'ES', 'UY', 'VE', 'US', 'OTHER',
        ];
    }

    /**
     * @param  array<int, string|null>  $headerRow
     * @return array<int, string>
     */
    private static function buildColumnKeyMap(array $headerRow): array
    {
        $map = [];
        foreach ($headerRow as $i => $label) {
            $label = (string) $label;
            if ($i === 0) {
                $label = preg_replace('/^\x{FEFF}/u', '', $label) ?? $label;
            }
            $mk = self::headerMatchKeyFromLabel($label);
            $map[$i] = match (true) {
                $mk === 'nombre' => 'nombre',
                $mk === 'apellidos' => 'apellidos',
                $mk === 'email' => 'email',
                $mk === 'telefono' => 'telefono',
                $mk === 'pais' => 'pais',
                str_contains($mk, 'wallet') && str_contains($mk, 'trc20') => 'wallet_trc20',
                str_contains($mk, 'fecha inicio') => 'fecha_inicio',
                str_contains($mk, 'duracion') => 'duracion',
                str_contains($mk, 'fecha final') => 'fecha_fin',
                str_contains($mk, 'monto plan') => 'monto_plan',
                str_contains($mk, 'saldo disponible') || str_contains($mk, 'ganancias') => 'saldo_ganancias',
                str_contains($mk, 'saldo total') => 'saldo_total',
                str_contains($mk, 'saldo bloqueado') => 'saldo_bloqueado',
                str_contains($mk, 'saldo pagado') => 'saldo_pagado',
                default => 'col_'.$i,
            };
        }

        return $map;
    }

    /**
     * @param  array<int, string>  $columnKeys
     * @param  array<int, string|null>  $cells
     * @return array<string, string>
     */
    private static function rowToAssoc(array $columnKeys, array $cells): array
    {
        $out = [];
        foreach ($columnKeys as $i => $key) {
            $out[$key] = isset($cells[$i]) ? self::normalizeCsvCell((string) $cells[$i]) : '';
        }

        return $out;
    }

    /**
     * @param  array<int, string|null>  $cells
     */
    private static function rowIsEmpty(array $cells): bool
    {
        foreach ($cells as $c) {
            if (trim((string) $c) !== '') {
                return false;
            }
        }

        return true;
    }

    /**
     * @param  array<string, string>  $base
     * @param  array<string, string>  $row
     * @return array<string, string>
     */
    private static function mergePersonFromRow(array $base, array $row): array
    {
        $fields = [
            'first_name' => $row['nombre'] ?? '',
            'last_name' => $row['apellidos'] ?? '',
            'email' => $row['email'] ?? '',
            'contact_number' => $row['telefono'] ?? '',
            'country' => $row['pais'] ?? '',
            'wallet_address' => $row['wallet_trc20'] ?? '',
        ];

        foreach ($fields as $k => $v) {
            $v = trim($v);
            if ($v !== '') {
                $base[$k] = $v;
            }
        }

        return $base;
    }

    /**
     * @param  array<string, string>  $row
     * @param  array<string, string>  $person
     * @return array{
     *     amount: float,
     *     duration_months: int,
     *     confirmed_at: ?Carbon,
     *     maturity_at: ?Carbon,
     * }|null
     */
    private static function parsePlanRow(array $row, array $person): ?array
    {
        $amount = self::parseMoneyString($row['monto_plan'] ?? '');
        if ($amount === null || $amount <= 0) {
            return null;
        }

        $durRaw = $row['duracion'] ?? '';
        $indefinite = self::isIndefiniteDuration($durRaw);
        $durationMonths = self::parseDurationMonths($durRaw);
        if (! $indefinite && $durationMonths === null) {
            return null;
        }

        if ($indefinite) {
            $durationMonths = 12;
        } elseif ($durationMonths !== null && $durationMonths < 1) {
            return null;
        }

        $durationMonths = max(1, (int) $durationMonths);

        $confirmedRaw = $row['fecha_inicio'] ?? '';
        $confirmedAt = self::parseFlexibleDate($confirmedRaw);

        $endRaw = $row['fecha_fin'] ?? '';
        $maturityAt = self::parseFlexibleDate($endRaw);
        if ($maturityAt !== null && self::isSinVencimiento($endRaw)) {
            $maturityAt = null;
        }

        if ($indefinite) {
            $maturityAt = null;
        } elseif ($maturityAt === null && $confirmedAt !== null) {
            $maturityAt = $confirmedAt->copy()->addMonths($durationMonths);
        }

        return [
            'amount' => $amount,
            'duration_months' => $durationMonths,
            'confirmed_at' => $confirmedAt,
            'maturity_at' => $maturityAt,
        ];
    }

    /**
     * Checksum estable para TXID y detección de duplicados (misma semántica que el import CSV).
     */
    public static function computeStagingRowChecksum(
        string $email,
        int $csvRecordIndex,
        float $amount,
        int $durationMonths,
        ?Carbon $confirmedAt,
        ?Carbon $maturityAt,
    ): string {
        $email = self::normalizeEmail($email);

        return hash('sha256', json_encode([
            'email' => $email,
            'record' => $csvRecordIndex,
            'amount' => round($amount, 2),
            'confirmed' => $confirmedAt?->toIso8601String(),
            'duration' => $durationMonths,
            'maturity' => $maturityAt?->toIso8601String(),
        ], JSON_THROW_ON_ERROR));
    }

    public static function normalizeEmail(string $email): string
    {
        return mb_strtolower(trim($email));
    }

    private static function parseMoneyString(string $raw): ?float
    {
        $s = str_replace(["\t", "\n", "\r", '"', ' ', 'USD', 'usd'], '', trim($raw));
        $s = str_replace('$', '', $s);
        if ($s === '' || stripos($s, 'completado') !== false) {
            return null;
        }

        $lastComma = strrpos($s, ',');
        $lastDot = strrpos($s, '.');

        if ($lastComma !== false && $lastDot !== false) {
            if ($lastComma > $lastDot) {
                $s = str_replace('.', '', $s);
                $s = str_replace(',', '.', $s);
            } else {
                $s = str_replace(',', '', $s);
            }
        } elseif ($lastComma !== false && $lastDot === false) {
            $afterComma = substr($s, $lastComma + 1);
            if (strlen($afterComma) <= 2 && ctype_digit($afterComma)) {
                $s = str_replace(',', '.', $s);
            } else {
                $s = str_replace(',', '', $s);
            }
        }

        if (! is_numeric($s)) {
            return null;
        }

        return round((float) $s, 2);
    }

    private static function parseFlexibleDate(string $raw): ?Carbon
    {
        $s = trim(str_replace(["\t", '"'], '', $raw));
        if ($s === '' || self::isSinVencimiento($s)) {
            return null;
        }

        if (preg_match('/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/', $s, $m)) {
            $a = (int) $m[1];
            $b = (int) $m[2];
            $y = (int) $m[3];

            if ($a > 12 && $b <= 12) {
                $day = $a;
                $month = $b;
            } elseif ($b > 12 && $a <= 12) {
                $month = $a;
                $day = $b;
            } elseif ($a <= 12 && $b <= 12) {
                $month = $a;
                $day = $b;
            } else {
                return null;
            }

            try {
                return Carbon::createFromDate($y, $month, $day)->startOfDay();
            } catch (\Throwable) {
                return null;
            }
        }

        try {
            return Carbon::parse($s)->startOfDay();
        } catch (\Throwable) {
            return null;
        }
    }

    private static function parseDurationMonths(string $raw): ?int
    {
        $s = mb_strtolower(trim(str_replace(["\t", '"'], '', $raw)));
        if ($s === '') {
            return null;
        }

        if (preg_match('/(\d+)\s*mes(?:es)?/u', $s, $m)) {
            return max(1, (int) $m[1]);
        }

        return null;
    }

    private static function isIndefiniteDuration(string $raw): bool
    {
        $s = mb_strtolower(trim(str_replace("\t", '', $raw)));

        return str_contains($s, 'indefin');
    }

    private static function isSinVencimiento(string $raw): bool
    {
        $s = mb_strtolower(trim(str_replace("\t", '', $raw)));

        return str_contains($s, 'sin vencimiento');
    }
}
