<?php

namespace App\Services;

use App\Enums\ProviderWithdrawalStatusEnum;
use App\Exceptions\BankAccountException;
use App\Models\Bank;
use App\Models\Provider;
use App\Models\ProviderBankAccount;
use App\Models\ProviderWithdrawal;
use App\Services\Pagarme\PagarmeRecipientService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class ProviderBankAccountService
{
    public function __construct(
        private readonly PagarmeRecipientService $pagarmeRecipientService,
    ) {}

    public function getAll(Provider $provider): Collection
    {
        return $provider->bankAccounts()->with('bank')->get();
    }

    public function findForProvider(Provider $provider, int $id): ProviderBankAccount
    {
        $account = ProviderBankAccount::query()
            ->where('provider_id', $provider->id)
            ->with('bank')
            ->find($id);

        if (! $account) {
            throw new BankAccountException('bank_account_not_found', 404);
        }

        return $account;
    }

    //

    public function create(Provider $provider, array $data): ProviderBankAccount
    {
        $attributes = $this->buildAttributes($provider, $data);

        $this->guardAgainstDuplicate($provider, $attributes);

        $isFirstAccount = ! $provider->bankAccounts()->exists();

        return DB::transaction(function () use ($provider, $attributes, $isFirstAccount) {
            $account = ProviderBankAccount::create([
                ...$attributes,
                'provider_id' => $provider->id,
                'is_primary'  => $isFirstAccount,
                'is_active'   => true,
            ]);

            if ($account->is_primary) {
                $this->syncPrimaryToGateway($provider, $account);
            }

            return $account->fresh('bank');
        });
    }

    /**
     * Cadastro do prestador: a conta enviada no formulario nasce como principal.
     *
     * Aqui o recebedor e criado com os dados de endereco que vieram na propria requisicao
     * (no cadastro o endereco pode ainda nao estar persistido).
     */
    public function createPrimaryFromRecipientPayload(Provider $provider, array $data): ?ProviderBankAccount
    {
        $bankAccount = data_get($data, 'recipient_default_bank_account', []);

        $hasBankData = ! empty(data_get($bankAccount, 'bank')) && ! empty(data_get($bankAccount, 'account_number'));

        if (! $hasBankData || $provider->bankAccounts()->exists()) {
            return null;
        }

        $attributes = $this->buildAttributes($provider, [
            'bank_code'           => data_get($bankAccount, 'bank'),
            'type'                => data_get($bankAccount, 'type', 'checking'),
            'branch_number'       => data_get($bankAccount, 'branch_number'),
            'branch_check_digit'  => data_get($bankAccount, 'branch_check_digit'),
            'account_number'      => data_get($bankAccount, 'account_number'),
            'account_check_digit' => data_get($bankAccount, 'account_check_digit'),
        ]);

        $account = ProviderBankAccount::create([
            ...$attributes,
            'provider_id' => $provider->id,
            'is_primary'  => true,
            'is_active'   => true,
        ]);

        $account->markGatewaySyncAttempt();

        if (empty($provider->recipient_id)) {
            $this->pagarmeRecipientService->createRecipientForProvider($provider, [
                ...$data,
                'recipient_default_bank_account' => $account->toGatewayBankAccount(),
            ]);
        } else {
            $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $account);
        }

        $account->forceFill([
            'gateway_synced_at' => now(),
            'gateway_sync_hash' => $account->gatewayHash(),
        ])->save();

        return $account;
    }

    public function update(Provider $provider, int $id, array $data): ProviderBankAccount
    {
        $account = $this->findForProvider($provider, $id);

        $attributes = $this->buildAttributes($provider, $data, $account);

        $this->guardAgainstDuplicate($provider, $attributes, $account->id);

        $makePrimary = ! $account->is_primary && filter_var(data_get($data, 'is_primary', false), FILTER_VALIDATE_BOOLEAN);

        if ($makePrimary) {
            if (! $account->is_active) {
                throw new BankAccountException('bank_account_inactive');
            }

            $this->guardAgainstWithdrawalInProgress($provider);
        }

        return DB::transaction(function () use ($provider, $account, $attributes, $makePrimary) {
            $account->fill($attributes)->save();

            if ($account->is_primary) {
                $this->syncPrimaryToGateway($provider, $account);
            } elseif ($makePrimary) {
                $this->promoteToPrimary($provider, $account);
            }

            return $account->fresh('bank');
        });
    }

    public function delete(Provider $provider, int $id): bool
    {
        $account = $this->findForProvider($provider, $id);

        $this->guardAgainstWithdrawalInProgress($provider);

        if ($provider->bankAccounts()->count() <= 1) {
            throw new BankAccountException('bank_account_last_cannot_be_deleted');
        }

        if ($account->is_primary) {
            throw new BankAccountException('bank_account_primary_requires_replacement');
        }

        return (bool) $account->delete();
    }

    //

    /**
     * Confere, direto no Pagar.me, se a conta padrao do recebedor e mesmo a conta
     * principal daqui - e reenvia se estiver divergente.
     *
     * A transacao do banco garante que nada seja gravado localmente sem o gateway ter
     * aceitado, mas nao cobre o caminho inverso: o PATCH ser aplicado e o commit local
     * falhar depois. Como quem decide o destino da transferencia e o gateway, essa
     * conferencia roda antes de cada saque, que e o unico momento em que isso importa.
     */
    public function ensureGatewayMatchesPrimary(Provider $provider): ProviderBankAccount
    {
        $account = $provider->primaryBankAccount()->first();

        if (! $account || ! $account->is_active) {
            throw new BankAccountException('provider_requires_synced_bank_account');
        }

        try {
            $remote = data_get(
                $this->pagarmeRecipientService->getRecipient($provider->recipient_id),
                'default_bank_account',
                [],
            );

            if (! $this->matchesGatewayAccount($account, $remote)) {
                DB::transaction(fn () => $this->syncPrimaryToGateway($provider, $account));
            }
        } catch (\Throwable $e) {
            Log::error('Falha ao conferir a conta bancaria do recebedor no Pagar.me', [
                'provider_id'             => $provider->id,
                'provider_bank_account_id'=> $account->id,
                'exception'               => $e->getMessage(),
            ]);

            throw new BankAccountException('provider_requires_synced_bank_account');
        }

        return $account->fresh();
    }

    private function matchesGatewayAccount(ProviderBankAccount $account, array $remote): bool
    {
        $local = $account->toGatewayBankAccount();

        foreach (['bank', 'branch_number', 'account_number', 'account_check_digit', 'type'] as $field) {
            if (ltrim((string) data_get($local, $field), '0') !== ltrim((string) data_get($remote, $field), '0')) {
                return false;
            }
        }

        return true;
    }

    //

    /**
     * O Pagar.me tem uma conta so por recebedor: promover uma conta significa
     * desmarcar a anterior e enviar a nova como conta padrao das transferencias.
     */
    private function promoteToPrimary(Provider $provider, ProviderBankAccount $account): void
    {
        ProviderBankAccount::query()
            ->where('provider_id', $provider->id)
            ->where('id', '!=', $account->id)
            ->where('is_primary', true)
            ->update(['is_primary' => false]);

        $account->forceFill(['is_primary' => true])->save();

        $this->syncPrimaryToGateway($provider, $account);
    }

    /**
     * Envia a conta principal ao Pagar.me. O recebedor continua sendo um so por prestador:
     * o que muda e a conta padrao que recebe as transferencias (saques).
     */
    private function syncPrimaryToGateway(Provider $provider, ProviderBankAccount $account): void
    {
        $account->markGatewaySyncAttempt();

        if (empty($provider->recipient_id)) {
            $this->pagarmeRecipientService->createRecipientForProvider(
                $provider,
                $this->buildRecipientDataFromProvider($provider, $account),
            );
        } else {
            $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $account);
        }

        $account->forceFill([
            'gateway_synced_at' => now(),
            'gateway_sync_hash' => $account->gatewayHash(),
        ])->save();
    }

    private function buildRecipientDataFromProvider(Provider $provider, ProviderBankAccount $account): array
    {
        $provider->loadMissing(['user', 'addresses.city', 'addresses.state']);

        $address = $provider->addresses->first();

        return [
            'recipient_name'                 => $provider->user?->name,
            'recipient_email'                => $provider->user?->email,
            'recipient_document'             => $provider->document,
            'recipient_type'                 => 'individual',
            'recipient_payment_mode'         => 'bank_transfer',
            'recipient_metadata'             => [],
            'recipient_default_bank_account' => $account->toGatewayBankAccount(),
            'birth_date'                     => $provider->birth_date,
            'phone'                          => $provider->user?->phone,
            'address'                        => $address?->address,
            'number'                         => $address?->number,
            'district'                       => $address?->district,
            'complement'                     => $address?->complement,
            'city'                           => $address?->city?->name,
            'state'                          => $address?->state?->code,
            'zip_code'                       => $address?->zip_code,
        ];
    }

    //

    /**
     * O titular e sempre o proprio prestador: o Pagar.me exige que o documento da conta
     * seja o mesmo documento do recebedor, entao nada disso vem do cliente.
     */
    private function buildAttributes(Provider $provider, array $data, ?ProviderBankAccount $current = null): array
    {
        $bank = $this->resolveBank($data, $current);

        $provider->loadMissing('user');

        return [
            'bank_id'             => $bank->id,
            'bank_code'           => $bank->code,
            'bank_name'           => $bank->name,
            'holder_name'         => trim((string) $provider->user?->name),
            'holder_document'     => $this->digits($provider->document),
            'holder_type'         => 'individual',
            'type'                => data_get($data, 'type', $current?->type),
            'branch_number'       => $this->digits(data_get($data, 'branch_number', $current?->branch_number)),
            'branch_check_digit'  => $this->digits(data_get($data, 'branch_check_digit', $current?->branch_check_digit)) ?: null,
            'account_number'      => $this->digits(data_get($data, 'account_number', $current?->account_number)),
            'account_check_digit' => $this->digits(data_get($data, 'account_check_digit', $current?->account_check_digit)),
        ];
    }

    private function resolveBank(array $data, ?ProviderBankAccount $current = null): Bank
    {
        $bankId   = data_get($data, 'bank_id');
        $bankCode = data_get($data, 'bank_code');

        if (! $bankId && ! $bankCode) {
            $bankId   = $current?->bank_id;
            $bankCode = $current?->bank_code;
        }

        $bank = Bank::query()
            ->where('active', true)
            ->when($bankId, fn ($query) => $query->where('id', $bankId))
            ->when(! $bankId && $bankCode, fn ($query) => $query->where('code', str_pad((string) $bankCode, 3, '0', STR_PAD_LEFT)))
            ->first();

        if (! $bank) {
            throw new BankAccountException('bank_not_found');
        }

        return $bank;
    }

    private function guardAgainstDuplicate(Provider $provider, array $attributes, ?int $ignoreId = null): void
    {
        $exists = ProviderBankAccount::query()
            ->where('provider_id', $provider->id)
            ->where('bank_code', data_get($attributes, 'bank_code'))
            ->where('branch_number', data_get($attributes, 'branch_number'))
            ->where('account_number', data_get($attributes, 'account_number'))
            ->where('account_check_digit', data_get($attributes, 'account_check_digit'))
            ->when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId))
            ->exists();

        if ($exists) {
            throw new BankAccountException('bank_account_already_registered');
        }
    }

    private function guardAgainstWithdrawalInProgress(Provider $provider): void
    {
        $inProgress = ProviderWithdrawal::query()
            ->where('provider_id', $provider->id)
            ->whereIn('status', [
                ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
                ProviderWithdrawalStatusEnum::PROCESSING,
            ])
            ->exists();

        if ($inProgress) {
            throw new BankAccountException('bank_account_blocked_by_withdrawal');
        }
    }

    private function digits(?string $value): string
    {
        return preg_replace('/\D+/', '', (string) $value) ?? '';
    }
}
