<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\FixedAsset;
use App\Models\FixedAssetDepreciation;
use App\Models\FixedAssetMaintenance;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class FixedAssetService
{
    public function __construct(
        private MoneyMath $money,
        private BranchContext $branchContext,
    ) {}

    public function create(array $data, User $user): FixedAsset
    {
        $costMinor = $this->money->toMinor((string) $data['acquisition_cost']);
        $residualMinor = $this->money->toMinor((string) ($data['residual_value'] ?? '0'));
        if ($costMinor <= 0) throw ValidationException::withMessages(['acquisition_cost' => 'Acquisition cost must be greater than zero.']);
        if ($residualMinor > $costMinor) throw ValidationException::withMessages(['residual_value' => 'Residual value cannot exceed acquisition cost.']);

        return $this->branchContext->forBranch((int) $data['branch_id'], fn () => DB::transaction(function () use ($data, $user, $costMinor, $residualMinor): FixedAsset {
            $branch = Branch::findOrFail((int) $data['branch_id']);
            $source = (string) ($data['acquisition_source'] ?? 'opening');
            if (! array_key_exists($source, FixedAsset::ACQUISITION_SOURCES)) {
                throw ValidationException::withMessages(['acquisition_source' => 'Unsupported asset acquisition source.']);
            }

            $account = null;
            $accountBalance = null;
            if ($source === 'financial_account') {
                if (empty($data['financial_account_id'])) throw ValidationException::withMessages(['financial_account_id' => 'A paying account is required for purchased assets.']);
                $account = FinancialAccount::query()->lockForUpdate()->findOrFail((int) $data['financial_account_id']);
                if ((int) $account->branch_id !== (int) $branch->id) throw ValidationException::withMessages(['financial_account_id' => 'The purchase account must belong to the asset branch.']);
                if ($account->status !== 'active') throw ValidationException::withMessages(['financial_account_id' => 'The purchase account must be active.']);
                $currentMinor = $this->money->toMinor((string) $account->current_balance);
                if ($currentMinor < $costMinor) throw ValidationException::withMessages(['financial_account_id' => 'The selected account has insufficient funds for this asset purchase.']);
                $accountBalance = $this->money->fromMinor($currentMinor - $costMinor);
            }

            $method = (string) ($data['depreciation_method'] ?? 'straight_line');
            if (! array_key_exists($method, FixedAsset::DEPRECIATION_METHODS)) throw ValidationException::withMessages(['depreciation_method' => 'Unsupported depreciation method.']);
            $life = $method === 'straight_line' ? (int) ($data['useful_life_months'] ?? 0) : null;
            if ($method === 'straight_line' && $life < 1) throw ValidationException::withMessages(['useful_life_months' => 'Useful life is required for straight-line depreciation.']);

            $assetNumber = $this->nextSequenceNumber('fixed_asset', 'AST', $branch);
            $asset = FixedAsset::create([
                'business_id' => $branch->business_id,'branch_id' => $branch->id,'asset_number' => $assetNumber,
                'name' => $data['name'],'category' => $data['category'],'description' => $data['description'] ?? null,
                'serial_number' => $data['serial_number'] ?? null,'supplier' => $data['supplier'] ?? null,
                'purchase_reference' => $data['purchase_reference'] ?? null,'acquisition_source' => $source,
                'acquisition_date' => $data['acquisition_date'],'depreciation_start_date' => $data['depreciation_start_date'] ?? $data['acquisition_date'],
                'acquisition_cost' => $this->money->fromMinor($costMinor),'residual_value' => $this->money->fromMinor($residualMinor),
                'depreciation_method' => $method,'useful_life_months' => $life,'accumulated_depreciation' => '0.00','book_value' => $this->money->fromMinor($costMinor),
                'financial_account_id' => $account?->id,'location' => $data['location'] ?? null,'custodian_user_id' => $data['custodian_user_id'] ?? null,
                'status' => $data['status'] ?? 'active','next_maintenance_date' => $data['next_maintenance_date'] ?? null,'notes' => $data['notes'] ?? null,
                'created_by' => $user->id,'updated_by' => $user->id,
            ]);

            $amount = $this->money->fromMinor($costMinor);
            $reference = ! empty($data['purchase_reference']) ? (string) $data['purchase_reference'] : $assetNumber;
            $journal = 'JRN-'.$assetNumber;
            $description = 'Fixed asset acquisition '.$assetNumber.' · '.$asset->name;

            AccountingEntry::create([
                'journal_number' => $journal,'branch_id' => $branch->id,'financial_account_id' => null,'entry_date' => $data['acquisition_date'],
                'reference' => $reference,'category' => 'fixed_assets','event_type' => 'fixed_asset_acquisition','description' => $description,
                'debit' => $amount,'credit' => '0.00','user_id' => $user->id,'related_type' => FixedAsset::class,'related_id' => $asset->id,
            ]);

            if ($account) {
                $account->update(['current_balance' => $accountBalance, 'updated_by' => $user->id]);
                FinancialAccountTransaction::create([
                    'transaction_number' => $this->nextSequenceNumber('financial_transaction', 'FTX', $branch),
                    'account_id' => $account->id,'branch_id' => $branch->id,'transaction_date' => $data['acquisition_date'],'reference' => $reference,
                    'type' => 'fixed_asset_purchase','description' => $description,'money_in' => '0.00','money_out' => $amount,'running_balance' => $accountBalance,
                    'user_id' => $user->id,'related_type' => FixedAsset::class,'related_id' => $asset->id,
                ]);
                AccountingEntry::create([
                    'journal_number' => $journal,'branch_id' => $branch->id,'financial_account_id' => $account->id,'entry_date' => $data['acquisition_date'],
                    'reference' => $reference,'category' => 'assets','event_type' => 'fixed_asset_acquisition','description' => $description,
                    'debit' => '0.00','credit' => $amount,'user_id' => $user->id,'related_type' => FixedAsset::class,'related_id' => $asset->id,
                ]);
            } else {
                AccountingEntry::create([
                    'journal_number' => $journal,'branch_id' => $branch->id,'financial_account_id' => null,'entry_date' => $data['acquisition_date'],
                    'reference' => $reference,'category' => 'capital','event_type' => 'fixed_asset_opening','description' => $description,
                    'debit' => '0.00','credit' => $amount,'user_id' => $user->id,'related_type' => FixedAsset::class,'related_id' => $asset->id,
                ]);
            }

            return $asset->fresh(['branch','financialAccount','custodian']);
        }));
    }

    public function postDepreciation(FixedAsset $asset, string $period, ?string $notes, User $user): FixedAssetDepreciation
    {
        return $this->branchContext->forBranch((int) $asset->branch_id, fn () => DB::transaction(function () use ($asset, $period, $notes, $user): FixedAssetDepreciation {
            $locked = FixedAsset::query()->lockForUpdate()->findOrFail($asset->id);
            if (! $locked->isDepreciable()) throw ValidationException::withMessages(['period' => 'This asset is not configured for depreciation.']);
            if ($locked->status === 'disposed') throw ValidationException::withMessages(['period' => 'Disposed assets cannot receive new depreciation postings.']);

            $periodDate = CarbonImmutable::parse($period.'-01')->startOfMonth();
            $startDate = CarbonImmutable::parse($locked->depreciation_start_date ?? $locked->acquisition_date)->startOfMonth();
            if ($periodDate->lt($startDate)) throw ValidationException::withMessages(['period' => 'Depreciation period cannot be before the depreciation start month.']);

            $index = (int) $startDate->diffInMonths($periodDate);
            $life = (int) $locked->useful_life_months;
            if ($index >= $life) throw ValidationException::withMessages(['period' => 'The selected month is outside this asset’s useful-life schedule.']);
            if (FixedAssetDepreciation::where('fixed_asset_id', $locked->id)->whereDate('period_date', $periodDate->toDateString())->exists()) {
                throw ValidationException::withMessages(['period' => 'Depreciation has already been posted for this asset and month.']);
            }

            $costMinor = $this->money->toMinor((string) $locked->acquisition_cost);
            $residualMinor = $this->money->toMinor((string) $locked->residual_value);
            $accumulatedMinor = $this->money->toMinor((string) $locked->accumulated_depreciation);
            $depreciableMinor = max(0, $costMinor - $residualMinor);
            $schedule = $this->money->allocate($depreciableMinor, $life);
            $amountMinor = min($schedule[$index], max(0, $depreciableMinor - $accumulatedMinor));
            if ($amountMinor <= 0) throw ValidationException::withMessages(['period' => 'This asset has no remaining depreciable amount.']);

            $newAccumulated = $accumulatedMinor + $amountMinor;
            $bookMinor = max($residualMinor, $costMinor - $newAccumulated);
            $amount = $this->money->fromMinor($amountMinor);
            $journal = 'JRN-DEP-'.$locked->asset_number.'-'.$periodDate->format('Ym');
            $reference = $locked->asset_number.'-'.$periodDate->format('Ym');
            $description = 'Depreciation '.$locked->asset_number.' for '.$periodDate->format('M Y');

            $record = FixedAssetDepreciation::create([
                'fixed_asset_id' => $locked->id,'period_date' => $periodDate,'amount' => $amount,
                'accumulated_after' => $this->money->fromMinor($newAccumulated),'book_value_after' => $this->money->fromMinor($bookMinor),
                'journal_number' => $journal,'notes' => $notes,'posted_at' => now(),'posted_by' => $user->id,
            ]);
            $locked->update(['accumulated_depreciation' => $this->money->fromMinor($newAccumulated),'book_value' => $this->money->fromMinor($bookMinor),'updated_by' => $user->id]);

            AccountingEntry::create([
                'journal_number' => $journal,'branch_id' => $locked->branch_id,'financial_account_id' => null,'entry_date' => $periodDate->endOfMonth(),
                'reference' => $reference,'category' => 'depreciation_expense','event_type' => 'fixed_asset_depreciation','description' => $description,
                'debit' => $amount,'credit' => '0.00','user_id' => $user->id,'related_type' => FixedAssetDepreciation::class,'related_id' => $record->id,
            ]);
            AccountingEntry::create([
                'journal_number' => $journal,'branch_id' => $locked->branch_id,'financial_account_id' => null,'entry_date' => $periodDate->endOfMonth(),
                'reference' => $reference,'category' => 'accumulated_depreciation','event_type' => 'fixed_asset_depreciation','description' => $description,
                'debit' => '0.00','credit' => $amount,'user_id' => $user->id,'related_type' => FixedAssetDepreciation::class,'related_id' => $record->id,
            ]);

            return $record->fresh(['asset','poster']);
        }));
    }

    public function recordMaintenance(FixedAsset $asset, array $data, User $user): FixedAssetMaintenance
    {
        $costMinor = $this->money->toMinor((string) ($data['cost'] ?? '0'));

        return $this->branchContext->forBranch((int) $asset->branch_id, fn () => DB::transaction(function () use ($asset, $data, $user, $costMinor): FixedAssetMaintenance {
            $lockedAsset = FixedAsset::query()->lockForUpdate()->findOrFail($asset->id);
            if ($lockedAsset->status === 'disposed') throw ValidationException::withMessages(['maintenance_date' => 'Disposed assets cannot receive maintenance postings.']);

            $account = null;
            $newBalance = null;
            if ($costMinor > 0) {
                if (empty($data['financial_account_id'])) throw ValidationException::withMessages(['financial_account_id' => 'A paying account is required when maintenance has a cost.']);
                $account = FinancialAccount::query()->lockForUpdate()->findOrFail((int) $data['financial_account_id']);
                if ((int) $account->branch_id !== (int) $lockedAsset->branch_id || $account->status !== 'active') {
                    throw ValidationException::withMessages(['financial_account_id' => 'Select an active account from the same branch as the asset.']);
                }
                $currentMinor = $this->money->toMinor((string) $account->current_balance);
                if ($currentMinor < $costMinor) throw ValidationException::withMessages(['financial_account_id' => 'The selected account has insufficient funds for this maintenance cost.']);
                $newBalance = $this->money->fromMinor($currentMinor - $costMinor);
            }

            $branch = $lockedAsset->branch()->firstOrFail();
            $maintenanceNumber = $this->nextSequenceNumber('fixed_asset_maintenance', 'MNT', $branch);
            $reference = ! empty($data['reference']) ? (string) $data['reference'] : $maintenanceNumber;
            $journal = $costMinor > 0 ? 'JRN-'.$maintenanceNumber : null;
            $amount = $this->money->fromMinor($costMinor);
            $record = FixedAssetMaintenance::create([
                'maintenance_number' => $maintenanceNumber,'fixed_asset_id' => $lockedAsset->id,'branch_id' => $lockedAsset->branch_id,
                'maintenance_date' => $data['maintenance_date'],'maintenance_type' => $data['maintenance_type'],'description' => $data['description'],
                'supplier' => $data['supplier'] ?? null,'cost' => $amount,'financial_account_id' => $account?->id,'reference' => $reference,
                'journal_number' => $journal,'next_due_date' => $data['next_due_date'] ?? null,'notes' => $data['notes'] ?? null,'recorded_by' => $user->id,
            ]);

            $assetUpdates = ['updated_by' => $user->id];
            if (! empty($data['next_due_date'])) $assetUpdates['next_maintenance_date'] = $data['next_due_date'];
            $lockedAsset->update($assetUpdates);

            if ($account && $costMinor > 0) {
                $account->update(['current_balance' => $newBalance, 'updated_by' => $user->id]);
                $description = 'Fixed asset maintenance '.$lockedAsset->asset_number.' · '.$record->description;
                FinancialAccountTransaction::create([
                    'transaction_number' => $this->nextSequenceNumber('financial_transaction', 'FTX', $branch),
                    'account_id' => $account->id,'branch_id' => $lockedAsset->branch_id,'transaction_date' => $data['maintenance_date'],'reference' => $reference,
                    'type' => 'fixed_asset_maintenance','description' => $description,'money_in' => '0.00','money_out' => $amount,'running_balance' => $newBalance,
                    'user_id' => $user->id,'related_type' => FixedAssetMaintenance::class,'related_id' => $record->id,
                ]);
                AccountingEntry::create([
                    'journal_number' => $journal,'branch_id' => $lockedAsset->branch_id,'financial_account_id' => null,'entry_date' => $data['maintenance_date'],
                    'reference' => $reference,'category' => 'asset_maintenance','event_type' => 'fixed_asset_maintenance','description' => $description,
                    'debit' => $amount,'credit' => '0.00','user_id' => $user->id,'related_type' => FixedAssetMaintenance::class,'related_id' => $record->id,
                ]);
                AccountingEntry::create([
                    'journal_number' => $journal,'branch_id' => $lockedAsset->branch_id,'financial_account_id' => $account->id,'entry_date' => $data['maintenance_date'],
                    'reference' => $reference,'category' => 'assets','event_type' => 'fixed_asset_maintenance','description' => $description,
                    'debit' => '0.00','credit' => $amount,'user_id' => $user->id,'related_type' => FixedAssetMaintenance::class,'related_id' => $record->id,
                ]);
            }

            return $record->fresh(['asset','branch','financialAccount','recorder']);
        }));
    }

    public function monthlyDepreciation(FixedAsset $asset, ?string $period = null): string
    {
        if (! $asset->isDepreciable()) return '0.00';
        $costMinor = $this->money->toMinor((string) $asset->acquisition_cost);
        $residualMinor = $this->money->toMinor((string) $asset->residual_value);
        $schedule = $this->money->allocate(max(0, $costMinor - $residualMinor), (int) $asset->useful_life_months);
        if ($period === null) return $this->money->fromMinor($schedule[0] ?? 0);
        $start = CarbonImmutable::parse($asset->depreciation_start_date ?? $asset->acquisition_date)->startOfMonth();
        $target = CarbonImmutable::parse($period.'-01')->startOfMonth();
        if ($target->lt($start)) return '0.00';
        $index = (int) $start->diffInMonths($target);
        return $this->money->fromMinor($schedule[$index] ?? 0);
    }

    private function nextSequenceNumber(string $type, string $prefix, Branch $branch): string
    {
        $year = (int) now()->format('Y');
        DB::table('number_sequences')->insertOrIgnore([
            'type' => $type,'branch_id' => $branch->id,'year' => $year,'current_value' => 0,'created_at' => now(),'updated_at' => now(),
        ]);
        $row = DB::table('number_sequences')->where('type', $type)->where('branch_id', $branch->id)->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()]);
        $tag = strtoupper(preg_replace('/[^A-Z0-9]/', '', str_replace('BR-', '', (string) $branch->code)) ?: (string) $branch->id);
        return $prefix.'-'.$tag.'-'.$year.'-'.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }
}
