<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\CapitalInjection;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\FinancialTransfer;
use App\Models\LoanDisbursement;
use App\Models\LoanPayment;
use App\Models\User;
use App\Support\Authorization\PermissionName;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class FinancialAccountingService
{
    public function __construct(private MoneyMath $money, private BranchAccessService $branchAccess) {}

    public function createAccount(array $data, User $user): FinancialAccount
    {
        Gate::forUser($user)->authorize(PermissionName::ACCOUNTS_MANAGE->value);
        $this->branchAccess->assertBranchAccessible($user, (int) $data['branch_id']);

        return DB::transaction(function () use ($data, $user): FinancialAccount {
            $openingMinor=$this->positiveOrZero($data['opening_balance'] ?? '0'); $opening=$this->money->fromMinor($openingMinor);
            $account=FinancialAccount::create(['branch_id'=>$data['branch_id'],'name'=>$data['name'],'account_type'=>$data['account_type'],'opening_balance'=>$opening,'current_balance'=>$opening,'status'=>$data['status'] ?? 'active','reference_details'=>$data['reference_details'] ?? null,'created_by'=>$user->id,'updated_by'=>$user->id]);
            if($openingMinor>0){
                $transactionNumber=$this->nextTransactionNumber($account->branch_id); $description='Opening balance for '.$account->name;
                FinancialAccountTransaction::create(['transaction_number'=>$transactionNumber,'account_id'=>$account->id,'branch_id'=>$account->branch_id,'transaction_date'=>now(),'reference'=>$transactionNumber,'type'=>'opening_balance','description'=>$description,'money_in'=>$opening,'money_out'=>'0.00','running_balance'=>$opening,'user_id'=>$user->id,'related_type'=>FinancialAccount::class,'related_id'=>$account->id]);
                $this->postCapitalJournal($account,$opening,now(),'JRN-'.$transactionNumber,$transactionNumber,'opening_balance',$description,$user,$account);
            }
            return $account->fresh();
        });
    }

    public function injectCapital(FinancialAccount $account, string $amount, CarbonInterface $date, ?string $reference, ?string $notes, User $user): CapitalInjection
    {
        Gate::forUser($user)->authorize('capital', $account);
        $amountMinor=$this->positive($amount);
        return DB::transaction(function () use ($account,$amountMinor,$date,$reference,$notes,$user): CapitalInjection {
            $locked=FinancialAccount::query()->lockForUpdate()->findOrFail($account->id);
            if($locked->status!=='active') throw ValidationException::withMessages(['account'=>'Capital can only be injected into an active account.']);
            $currentMinor=$this->money->toMinor((string)$locked->current_balance); $newBalance=$this->money->fromMinor($currentMinor+$amountMinor); $amountValue=$this->money->fromMinor($amountMinor);
            $capitalNumber=$this->uniqueSourceNumber('CAP',CapitalInjection::class,'capital_number'); $transactionNumber=$this->nextTransactionNumber($locked->branch_id); $description=$notes ?: 'Business capital injection'; $effectiveReference=$reference ?: $capitalNumber;
            $capital=CapitalInjection::create(['capital_number'=>$capitalNumber,'account_id'=>$locked->id,'branch_id'=>$locked->branch_id,'amount'=>$amountValue,'injection_date'=>$date,'reference'=>$effectiveReference,'notes'=>$notes,'created_by'=>$user->id]);
            $locked->update(['current_balance'=>$newBalance,'updated_by'=>$user->id]);
            FinancialAccountTransaction::create(['transaction_number'=>$transactionNumber,'account_id'=>$locked->id,'branch_id'=>$locked->branch_id,'transaction_date'=>$date,'reference'=>$effectiveReference,'type'=>'capital_injection','description'=>$description,'money_in'=>$amountValue,'money_out'=>'0.00','running_balance'=>$newBalance,'user_id'=>$user->id,'related_type'=>CapitalInjection::class,'related_id'=>$capital->id]);
            $this->postCapitalJournal($locked,$amountValue,$date,'JRN-'.$capitalNumber,$effectiveReference,'capital_injection',$description,$user,$capital);
            return $capital->fresh(['account','branch']);
        });
    }

    public function transfer(FinancialAccount $source, FinancialAccount $destination, string $amount, CarbonInterface $date, ?string $reference, ?string $notes, User $user): FinancialTransfer
    {
        Gate::forUser($user)->authorize('transfer', $source);
        Gate::forUser($user)->authorize('transfer', $destination);
        if($source->id===$destination->id) throw ValidationException::withMessages(['destination_account_id'=>'Source and destination accounts must be different.']);
        $amountMinor=$this->positive($amount);
        return DB::transaction(function () use ($source,$destination,$amountMinor,$date,$reference,$notes,$user): FinancialTransfer {
            $ids=[$source->id,$destination->id]; sort($ids); $locked=FinancialAccount::query()->whereIn('id',$ids)->orderBy('id')->lockForUpdate()->get()->keyBy('id'); $from=$locked->get($source->id); $to=$locked->get($destination->id);
            if(!$from || !$to) throw ValidationException::withMessages(['account'=>'One of the selected accounts no longer exists.']);
            if($from->status!=='active' || $to->status!=='active') throw ValidationException::withMessages(['account'=>'Transfers require active source and destination accounts.']);
            if($from->branch->business_id!==$to->branch->business_id) throw ValidationException::withMessages(['destination_account_id'=>'Transfers cannot cross businesses.']);
            $sourceMinor=$this->money->toMinor((string)$from->current_balance); if($sourceMinor<$amountMinor) throw ValidationException::withMessages(['amount'=>'The source account has insufficient funds.']);
            $destinationMinor=$this->money->toMinor((string)$to->current_balance); $amountValue=$this->money->fromMinor($amountMinor); $sourceBalance=$this->money->fromMinor($sourceMinor-$amountMinor); $destinationBalance=$this->money->fromMinor($destinationMinor+$amountMinor);
            $transferNumber=$this->uniqueSourceNumber('TRF',FinancialTransfer::class,'transfer_number'); $effectiveReference=$reference ?: $transferNumber; $description=$notes ?: 'Transfer '.$from->name.' → '.$to->name;
            $transfer=FinancialTransfer::create(['transfer_number'=>$transferNumber,'source_account_id'=>$from->id,'destination_account_id'=>$to->id,'amount'=>$amountValue,'transfer_date'=>$date,'reference'=>$effectiveReference,'notes'=>$notes,'created_by'=>$user->id]);
            $from->update(['current_balance'=>$sourceBalance,'updated_by'=>$user->id]); $to->update(['current_balance'=>$destinationBalance,'updated_by'=>$user->id]);
            FinancialAccountTransaction::create(['transaction_number'=>$this->nextTransactionNumber($from->branch_id),'account_id'=>$from->id,'branch_id'=>$from->branch_id,'transaction_date'=>$date,'reference'=>$effectiveReference,'type'=>'transfer_out','description'=>$description,'money_in'=>'0.00','money_out'=>$amountValue,'running_balance'=>$sourceBalance,'user_id'=>$user->id,'transfer_id'=>$transfer->id,'related_type'=>FinancialTransfer::class,'related_id'=>$transfer->id]);
            FinancialAccountTransaction::create(['transaction_number'=>$this->nextTransactionNumber($to->branch_id),'account_id'=>$to->id,'branch_id'=>$to->branch_id,'transaction_date'=>$date,'reference'=>$effectiveReference,'type'=>'transfer_in','description'=>$description,'money_in'=>$amountValue,'money_out'=>'0.00','running_balance'=>$destinationBalance,'user_id'=>$user->id,'transfer_id'=>$transfer->id,'related_type'=>FinancialTransfer::class,'related_id'=>$transfer->id]);
            $journal='JRN-'.$transferNumber;
            AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$from->branch_id,'financial_account_id'=>$from->id,'entry_date'=>$date,'reference'=>$effectiveReference,'category'=>'assets','event_type'=>'account_transfer','description'=>$description,'debit'=>'0.00','credit'=>$amountValue,'user_id'=>$user->id,'related_type'=>FinancialTransfer::class,'related_id'=>$transfer->id]);
            AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$to->branch_id,'financial_account_id'=>$to->id,'entry_date'=>$date,'reference'=>$effectiveReference,'category'=>'assets','event_type'=>'account_transfer','description'=>$description,'debit'=>$amountValue,'credit'=>'0.00','user_id'=>$user->id,'related_type'=>FinancialTransfer::class,'related_id'=>$transfer->id]);
            return $transfer->fresh(['sourceAccount','destinationAccount']);
        });
    }

    public function postLoanDisbursement(LoanDisbursement $disbursement, FinancialAccount $account, string $amount, CarbonInterface $date, string $reference, ?string $notes, User $user): FinancialAccountTransaction
    {
        $amountMinor=$this->positive($amount);
        return DB::transaction(function () use ($disbursement,$account,$amountMinor,$date,$reference,$notes,$user): FinancialAccountTransaction {
            $locked=FinancialAccount::query()->lockForUpdate()->findOrFail($account->id);
            if($locked->branch_id!==$disbursement->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The disbursement account must belong to the loan branch.']);
            if($locked->status!=='active') throw ValidationException::withMessages(['financial_account_id'=>'Loan disbursement requires an active financial account.']);
            $currentMinor=$this->money->toMinor((string)$locked->current_balance); if($currentMinor<$amountMinor) throw ValidationException::withMessages(['amount'=>'The selected financial account has insufficient funds.']);
            $amountValue=$this->money->fromMinor($amountMinor); $newBalance=$this->money->fromMinor($currentMinor-$amountMinor); $transactionNumber=$this->nextTransactionNumber($locked->branch_id); $disbursement->loadMissing('loan'); $description=$notes ?: 'Loan disbursement '.$disbursement->loan->loan_number;
            $locked->update(['current_balance'=>$newBalance,'updated_by'=>$user->id]);
            $transaction=FinancialAccountTransaction::create(['transaction_number'=>$transactionNumber,'account_id'=>$locked->id,'branch_id'=>$locked->branch_id,'transaction_date'=>$date,'reference'=>$reference,'type'=>'loan_disbursement','description'=>$description,'money_in'=>'0.00','money_out'=>$amountValue,'running_balance'=>$newBalance,'user_id'=>$user->id,'related_type'=>LoanDisbursement::class,'related_id'=>$disbursement->id]);
            $journal='JRN-'.$disbursement->disbursement_number;
            AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$locked->branch_id,'financial_account_id'=>$locked->id,'entry_date'=>$date,'reference'=>$reference,'category'=>'assets','event_type'=>'loan_disbursement','description'=>$description,'debit'=>'0.00','credit'=>$amountValue,'user_id'=>$user->id,'related_type'=>LoanDisbursement::class,'related_id'=>$disbursement->id]);
            AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$locked->branch_id,'financial_account_id'=>null,'entry_date'=>$date,'reference'=>$reference,'category'=>'principal','event_type'=>'loan_disbursement','description'=>'Principal receivable for '.$disbursement->loan->loan_number,'debit'=>$amountValue,'credit'=>'0.00','user_id'=>$user->id,'related_type'=>LoanDisbursement::class,'related_id'=>$disbursement->id]);
            return $transaction;
        });
    }

    public function postLoanPayment(LoanPayment $payment, FinancialAccount $account, array $components, CarbonInterface $date, string $reference, ?string $notes, User $user): FinancialAccountTransaction
    {
        $componentMinor=[]; $totalMinor=0;
        foreach(['principal','interest','fees','penalties'] as $category){ $componentMinor[$category]=$this->money->toMinor((string)($components[$category] ?? '0')); if($componentMinor[$category]<0) throw ValidationException::withMessages(['amount'=>'Payment allocation cannot be negative.']); $totalMinor+=$componentMinor[$category]; }
        if($totalMinor<=0 || $totalMinor!==$this->money->toMinor((string)$payment->amount)) throw ValidationException::withMessages(['amount'=>'Payment allocation must reconcile exactly with the payment amount.']);

        return DB::transaction(function () use ($payment,$account,$componentMinor,$totalMinor,$date,$reference,$notes,$user): FinancialAccountTransaction {
            $locked=FinancialAccount::query()->lockForUpdate()->findOrFail($account->id);
            if($locked->branch_id!==$payment->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The receiving account must belong to the same branch as the loan.']);
            if($locked->status!=='active') throw ValidationException::withMessages(['financial_account_id'=>'Loan payments require an active receiving account.']);
            $currentMinor=$this->money->toMinor((string)$locked->current_balance); $newBalance=$this->money->fromMinor($currentMinor+$totalMinor); $amountValue=$this->money->fromMinor($totalMinor); $transactionNumber=$this->nextTransactionNumber($locked->branch_id);
            $payment->loadMissing('loan'); $description=$notes ?: 'Loan payment '.$payment->loan->loan_number;
            $locked->update(['current_balance'=>$newBalance,'updated_by'=>$user->id]);
            $transaction=FinancialAccountTransaction::create(['transaction_number'=>$transactionNumber,'account_id'=>$locked->id,'branch_id'=>$locked->branch_id,'transaction_date'=>$date,'reference'=>$reference,'type'=>'loan_payment','description'=>$description,'money_in'=>$amountValue,'money_out'=>'0.00','running_balance'=>$newBalance,'user_id'=>$user->id,'related_type'=>LoanPayment::class,'related_id'=>$payment->id]);
            $journal='JRN-'.$payment->receipt_number;
            AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$locked->branch_id,'financial_account_id'=>$locked->id,'entry_date'=>$date,'reference'=>$reference,'category'=>'assets','event_type'=>'loan_payment','description'=>$description,'debit'=>$amountValue,'credit'=>'0.00','user_id'=>$user->id,'related_type'=>LoanPayment::class,'related_id'=>$payment->id]);
            foreach($componentMinor as $category=>$minor){
                if($minor<=0) continue;
                AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$locked->branch_id,'financial_account_id'=>null,'entry_date'=>$date,'reference'=>$reference,'category'=>$category,'event_type'=>'loan_payment','description'=>ucfirst($category).' allocation for '.$payment->loan->loan_number,'debit'=>'0.00','credit'=>$this->money->fromMinor($minor),'user_id'=>$user->id,'related_type'=>LoanPayment::class,'related_id'=>$payment->id]);
            }
            return $transaction;
        });
    }

    private function postCapitalJournal(FinancialAccount $account, string $amount, CarbonInterface $date, string $journal, string $reference, string $eventType, string $description, User $user, Model $related): void
    {
        AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$account->branch_id,'financial_account_id'=>$account->id,'entry_date'=>$date,'reference'=>$reference,'category'=>'assets','event_type'=>$eventType,'description'=>$description,'debit'=>$amount,'credit'=>'0.00','user_id'=>$user->id,'related_type'=>$related::class,'related_id'=>$related->getKey()]);
        AccountingEntry::create(['journal_number'=>$journal,'branch_id'=>$account->branch_id,'financial_account_id'=>null,'entry_date'=>$date,'reference'=>$reference,'category'=>'capital','event_type'=>$eventType,'description'=>$description,'debit'=>'0.00','credit'=>$amount,'user_id'=>$user->id,'related_type'=>$related::class,'related_id'=>$related->getKey()]);
    }

    private function nextTransactionNumber(int $branchId): string
    {
        $year=(int)now()->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'financial_transaction','branch_id'=>$branchId,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row=DB::table('number_sequences')->where('type','financial_transaction')->where('branch_id',$branchId)->where('year',$year)->lockForUpdate()->first(); $next=((int)$row->current_value)+1;
        DB::table('number_sequences')->where('id',$row->id)->update(['current_value'=>$next,'updated_at'=>now()]);
        $branch=Branch::findOrFail($branchId); $tag=strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$branch->code)) ?: (string)$branchId);
        return 'FTX-'.$tag.'-'.$year.'-'.str_pad((string)$next,6,'0',STR_PAD_LEFT);
    }

    private function uniqueSourceNumber(string $prefix, string $modelClass, string $column): string
    {
        do { $number=$prefix.'-'.strtoupper(substr((string)Str::ulid(),-12)); } while($modelClass::where($column,$number)->exists());
        return $number;
    }

    private function positive(string|int $amount): int { $minor=$this->money->toMinor($amount); if($minor<=0) throw ValidationException::withMessages(['amount'=>'Amount must be greater than zero.']); return $minor; }
    private function positiveOrZero(string|int $amount): int { return $this->money->toMinor($amount); }
}
