<?php

/**
 * Invoice Ninja (https://invoiceninja.com).
 *
 * @link https://github.com/invoiceninja/invoiceninja source repository
 *
 * @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
 *
 * @license https://www.elastic.co/licensing/elastic-license
 *
 * Documentation of Api-Usage: https://developer.gocardless.com/bank-account-data/overview
 *
 * Institutions: Are Banks or Payment-Providers, which manages bankaccounts.
 *
 * Accounts: Accounts are existing bank_accounts at a specific institution.
 *
 * Requisitions: Are registered/active user-flows to authenticate one or many accounts. After completition, the accoundId could be used to fetch data for this account. After the access expires, the user could create a new requisition to connect accounts again.
 */

namespace App\Helpers\Bank\Nordigen;

use App\Models\Company;
use App\Services\Email\Email;
use App\Models\BankIntegration;
use App\Services\Email\EmailObject;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Mail\Mailables\Address;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
use App\Helpers\Bank\Nordigen\Transformer\TransactionTransformer;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\RateLimitError;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\AccessExpiredError;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\AccountInactiveError;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\UnknownRequestError;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\InstitutionServiceError;
use Nordigen\NordigenPHP\Exceptions\InstitutionExceptions\InstitutionConnectionError;

class Nordigen
{
    public bool $test_mode; // https://developer.gocardless.com/bank-account-data/sandbox

    public string $sandbox_institutionId = 'SANDBOXFINANCE_SFIN0000';

    protected \Nordigen\NordigenPHP\API\NordigenClient $client;

    public function __construct()
    {
        $this->test_mode = config('ninja.nordigen.test_mode');

        if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key'))) {
            throw new \Exception('missing nordigen credentials');
        }

        $this->client = new \Nordigen\NordigenPHP\API\NordigenClient(config('ninja.nordigen.secret_id'), config('ninja.nordigen.secret_key'));

        $this->client->createAccessToken();
    }

    // metadata-section for frontend
    public function getInstitutions()
    {
        if ($this->test_mode) {
            return [$this->client->institution->getInstitution($this->sandbox_institutionId)];
        }

        return $this->client->institution->getInstitutions();
    }

    /**
     * Get end user agreement details by ID.
     *
     * @return array{
     *   id: string,
     *   created: string,
     *   institution_id: string,
     *   max_historical_days: int,
     *   access_valid_for_days: int,
     *   access_scope: string[],
     *   accepted: string
     * } Agreement details
     */
    public function getAgreement(string $euaId): array
    {
        return $this->client->endUserAgreement->getEndUserAgreement($euaId);
    }

    /**
     * Get a list of end user agreements
     *
     * @return array{
     *   id: string,
     *   created: string,
     *   institution_id: string,
     *   max_historical_days: int,
     *   access_valid_for_days: int,
     *   access_scope: string[],
     *   accepted: ?string,
     * }[] EndUserAgreement list
     */
    public function firstValidAgreement(string $institutionId, int $accessDays, int $txDays): ?array
    {
        $requiredScopes = ['balances', 'details', 'transactions'];

        try {
            return Arr::first(
                $this->client->endUserAgreement->getEndUserAgreements()['results'],
                function (array $eua) use ($institutionId, $requiredScopes, $accessDays, $txDays): bool {
                    $isNotExpired = !isset($eua['status']) || $eua['status'] !== 'EXPIRED';

                    return $eua['institution_id'] === $institutionId
                       && $eua['accepted'] === null
                       && $isNotExpired
                       && $eua['max_historical_days'] >= $txDays
                       && $eua['access_valid_for_days'] >= $accessDays
                       && !array_diff($requiredScopes, $eua['access_scope'] ?? []);
                },
                null
            );
        } catch (\Exception $e) {
            $debug = "{$e->getMessage()} ({$e->getCode()})";

            nlog("Nordigen: Unable to fetch End User Agreements for institution '{$institutionId}': {$debug}");

            return null;
        }
    }

    /**
     * Create a new End User Agreement with the given parameters
     *
     * @param array{id: string, transaction_total_days: int, max_access_valid_for_days: int} $institution
     *
     * @throws \Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException
     *
     * @return array{
     *   id: string,
     *   created: string,
     *   institution_id: string,
     *   max_historical_days: int,
     *   access_valid_for_days: int,
     *   access_scope: string[],
     *   accepted: string
     * } Agreement details
     */
    public function createAgreement(array $institution, int $accessDays, int $transactionDays): array
    {
        $txDays = $transactionDays < 30 ? 30 : $transactionDays;
        $maxAccess = $institution['max_access_valid_for_days'];
        $maxTx = $institution['transaction_total_days'];

        return $this->client->endUserAgreement->createEndUserAgreement(
            accessValidForDays: $accessDays > $maxAccess ? $maxAccess : $accessDays,
            maxHistoricalDays: $txDays > $maxTx ? $maxTx : $txDays,
            institutionId: $institution['id'],
        );
    }

    /**
     * Create a new Bank Requisition
     *
     * @param array{id: ?string} $institution,
     * @param array{id: ?string, transaction_total_days: int} $agreement
     */
    public function createRequisition(
        string $redirect,
        array $institution,
        array $agreement,
        string $reference,
        string $userLanguage,
    ): array {
        if ($this->test_mode && $institution['id'] != $this->sandbox_institutionId) {
            throw new \Exception('invalid institutionId while in test-mode');
        }

        return $this->client->requisition->createRequisition(
            $redirect,
            $institution['id'],
            $agreement['id'] ?? null,
            $reference,
            $userLanguage
        );
    }


    /**
     * validAgreement
     * @param string $institution_id
     * @param array $_accounts
     * @return array|null
     * @todo - very expensive!
     */
    public function validAgreement($institution_id, $_accounts)
    {

        $nc = new \App\Helpers\Bank\Nordigen\Http\NordigenClient($this->client->getAccessToken());
        $requisitions = $nc->getAllRequisitions();

        $requisition = $requisitions->filter(function ($requisition) use ($institution_id, $_accounts) {
            if ($requisition['institution_id'] == $institution_id && !empty(array_intersect($requisition['accounts'], $_accounts))) {
                return $requisition;
            }
        });

        return $requisition->first() ??  null;

    }

    public function getRequisition(string $requisitionId)
    {
        try {
            return $this->client->requisition->getRequisition($requisitionId);
        } catch (\Exception $e) {
            if (strpos($e->getMessage(), 'Invalid Requisition ID') !== false) {
                return false;
            }

            throw $e;
        }
    }

    // TODO: return null on not found
    public function getAccount(string $account_id)
    {
        try {
            $out = new \stdClass();

            $out->metadata = $this->client->account($account_id)->getAccountMetaData();
            $out->institution = $this->client->institution->getInstitution($out->metadata['institution_id']);

            // if($out->metadata['status'] == 'READY'){
            //     $out->data = $this->client->account($account_id)->getAccountDetails()['account'];
            //     $out->balances = $this->client->account($account_id)->getAccountBalances()['balances'];
            // }
            // else{

            $out->data = [
                'iban' => $out->metadata['iban'],
                'ownerName' => $out->metadata['owner_name'],
            ];
            $out->balances = [
                [
                    'balanceType' => '',
                    'balanceAmount' => [
                        'amount' => 0,
                        'currency' => '',
                    ],
                ],
            ];
            // }

            $it = new AccountTransformer();
            return $it->transform($out);

        } catch (\GuzzleHttp\Exception\ClientException $e) {
            $response = $e->getResponse();
            $statusCode = $response->getStatusCode();

            if ($statusCode === 429) {
                nlog("Nordigen Rate Limit hit for account {$account_id}");
                return ['error' => 'Nordigen Institution Rate Limit Reached', 'code' => 429];
            }
        } catch (\Exception $e) {

            nlog("Nordigen getAccount() failed => {$account_id} => " . $e->getMessage());
            return ['error' => $e->getMessage(), 'requisition' => true, 'code' => 401];

        }
    }

    /**
     * isAccountActive
     *
     * @param  string $account_id
     * @return array
     */
    public function isAccountActive(string $account_id): array
    {
        try {
            $account = $this->client->account($account_id)->getAccountMetaData();

            if ($account['status'] != 'READY') {
                nlog("Nordigen account '{$account_id}' is not ready (status={$account['status']})");
            }

            return $account;

        } catch (RateLimitError $e) {

            nlog("Nordigen:: AccountActiveStatus:: rate limited for account {$account_id}");

            return ['status' => 'RATE_LIMITED', 'code' => 429];

        } catch (AccessExpiredError $e) {

            return ['status' => 'EXPIRED'];

        } catch (AccountInactiveError $e) {

            return ['status' => 'SUSPENDED', 'error' => $e->getMessage()];

        } catch (\Exception $e) {

            nlog("Nordigen:: AccountActiveStatus:: {$e->getMessage()} {$e->getCode()}");

            if (strpos($e->getMessage(), 'Invalid Account ID') !== false) {
                return ['status' => 'Invalid Account ID'];
            }

            // Do not collapse unknown/transient errors to EXPIRED — that disables healthy
            // accounts on a rate-limit/timeout/5xx and emails the user a false reconnect notice.
            // Leave the integration enabled; the requisition gate is the authority on permanent failure.
            return ['status' => 'TRANSIENT_ERROR', 'error' => $e->getMessage()];
        }
    }

    /**
     * Touch a scarce account-resource endpoint to nudge sticky ERROR accounts
     * without spending the transactions scope.
     *
     * @return array{status: string, code?: int, error?: string}
     */
    public function wakeAccount(string $accountId, ?string $knownStatus = null): array
    {
        try {
            $status = $knownStatus;

            if ($status === null) {
                $account = $this->client->account($accountId)->getAccountMetaData();
                $status = $account['status'] ?? 'UNKNOWN';
            }

            if ($status !== 'ERROR') {
                return ['status' => $status];
            }

            $this->client->account($accountId)->getAccountBalances();

            return ['status' => 'WAKE_PROBED'];

        } catch (RateLimitError $e) {

            return ['status' => 'WAKE_RATE_LIMITED', 'code' => 429, 'error' => $e->getMessage()];

        } catch (AccessExpiredError $e) {

            return ['status' => 'EXPIRED', 'error' => $e->getMessage()];

        } catch (AccountInactiveError $e) {

            return ['status' => 'SUSPENDED', 'error' => $e->getMessage()];

        } catch (InstitutionServiceError | InstitutionConnectionError | UnknownRequestError $e) {

            return $this->normalizeWakeException($e);

        } catch (\Exception $e) {

            return $this->normalizeWakeException($e);
        }
    }

    /**
     * @return array{status: string, error: string}
     */
    private function normalizeWakeException(\Exception $e): array
    {
        $message = $e->getMessage();
        $normalized_message = strtolower($message);

        if (strpos($message, 'Invalid Account ID') !== false) {
            return ['status' => 'Invalid Account ID', 'error' => $message];
        }

        if (strpos($normalized_message, 'suspended') !== false) {
            return ['status' => 'SUSPENDED', 'error' => $message];
        }

        if (strpos($normalized_message, 'expired') !== false || strpos($normalized_message, 'revoked') !== false) {
            return ['status' => 'EXPIRED', 'error' => $message];
        }

        return ['status' => 'WAKE_TRANSIENT_ERROR', 'error' => $message];
    }

    /**
     * Cached, deduped requisition status check.
     *
     * Keyed by requisition_id so multiple bank_integrations sharing a requisition trigger
     * a single upstream call. The key is the requisition_id, so a reconnect (which mints a
     * new requisition_id) is never served a stale cached result.
     */
    public function requisitionStatus(string $requisitionId): ?string
    {
        $cache_key = "nordigen_req_status:{$requisitionId}";

        if (Cache::has($cache_key)) {
            return Cache::get($cache_key);
        }

        $nc = new \App\Helpers\Bank\Nordigen\Http\NordigenClient($this->client->getAccessToken());

        $status = $nc->getRequisition($requisitionId)['status'] ?? null;

        if ($status !== null) {
            Cache::put($cache_key, $status, in_array($status, ['EX', 'SU', 'RJ'], true) ? 60 * 60 * 3 : 60 * 15);
        }

        return $status;
    }


    /**
     * getTransactions
     *
     * @param  string $accountId
     * @param  ?string $dateFrom
     * @return array
     */
    public function getTransactions(Company $company, string $accountId, ?string $dateFrom = null): array
    {
        $transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom);

        $it = new TransactionTransformer($company);
        return $it->transform($transactionResponse);
    }

    public function disabledAccountEmail(BankIntegration $bank_integration): void
    {
        $cache_key = "email_quota:{$bank_integration->account->key}:bank_integration_notified";

        if (Cache::has($cache_key)) {
            return;
        }

        Cache::put($cache_key, true, 60 * 60 * 24);

        App::setLocale($bank_integration->company->getLocale());

        $mo = new EmailObject();
        $mo->subject = ctrans('texts.nordigen_requisition_subject');
        $mo->body = ctrans('texts.nordigen_requisition_body');
        $mo->text_body = ctrans('texts.nordigen_requisition_body');
        $mo->company_key = $bank_integration->company->company_key;
        $mo->html_template = 'email.template.generic';
        $mo->to = [new Address($bank_integration->company->owner()->email, $bank_integration->company->owner()->present()->name())];
        $mo->email_template_body = 'nordigen_requisition_body';
        $mo->email_template_subject = 'nordigen_requisition_subject';

        Email::dispatch($mo, $bank_integration->company);



    }

}
