<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\EmployeeCompensation;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\PayrollEntry;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Support\Authorization\PermissionName;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class PayrollService
{
    private const COMPONENTS = [
        'basic_salary', 'allowances', 'bonus', 'commission', 'overtime',
        'deductions', 'advances', 'other_deductions',
    ];

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

    public function configureCompensation(User $employee, array $data, User $actor): EmployeeCompensation
    {
        Gate::forUser($actor)->authorize(PermissionName::PAYROLL_COMPENSATION_MANAGE->value);
        $employee->loadMissing('primaryBranch');
        if (! $employee->primaryBranch) {
            throw ValidationException::withMessages(['employee' => 'The employee must have a primary branch before compensation can be configured.']);
        }
        $this->branchAccess->assertBranchAccessible($actor, (int) $employee->primaryBranch->id);

        $amounts = $this->normalizedComponents($data);
        $existing = EmployeeCompensation::where('user_id', $employee->id)->first();

        return EmployeeCompensation::updateOrCreate(
            ['user_id' => $employee->id],
            $amounts + [
                'business_id' => $employee->primaryBranch->business_id,
                'effective_from' => $data['effective_from'] ?? null,
                'status' => $data['status'] ?? 'active',
                'notes' => $data['notes'] ?? null,
                'created_by' => $existing?->created_by ?: $actor->id,
                'updated_by' => $actor->id,
            ]
        );
    }

    public function createPeriod(array $data, User $creator): PayrollPeriod
    {
        Gate::forUser($creator)->authorize(PermissionName::PAYROLL_PERIODS_CREATE->value);
        $this->branchAccess->assertBranchAccessible($creator, (int) $data['branch_id']);

        $start = CarbonImmutable::parse((string) $data['start_date'])->startOfDay();
        $end = CarbonImmutable::parse((string) $data['end_date'])->startOfDay();
        $payDate = CarbonImmutable::parse((string) $data['pay_date'])->startOfDay();

        if ($end->lt($start)) {
            throw ValidationException::withMessages(['end_date' => 'Payroll period end date cannot be before the start date.']);
        }

        return DB::transaction(function () use ($data, $creator, $start, $end, $payDate): PayrollPeriod {
            $branch = Branch::query()->with('business')->lockForUpdate()->findOrFail($data['branch_id']);

            $overlap = PayrollPeriod::query()
                ->where('branch_id', $branch->id)
                ->where('status', '!=', 'rejected')
                ->whereDate('start_date', '<=', $end->toDateString())
                ->whereDate('end_date', '>=', $start->toDateString())
                ->exists();
            if ($overlap) {
                throw ValidationException::withMessages(['start_date' => 'This branch already has a non-rejected payroll period overlapping the selected dates.']);
            }

            $employees = User::query()
                ->where('primary_branch_id', $branch->id)
                ->where('status', 'active')
                ->orderBy('name')
                ->get();
            if ($employees->isEmpty()) {
                throw ValidationException::withMessages(['branch_id' => 'The selected branch has no active employees with this branch as their primary branch.']);
            }

            $compensations = EmployeeCompensation::query()
                ->whereIn('user_id', $employees->pluck('id'))
                ->where('business_id', $branch->business_id)
                ->where('status', 'active')
                ->where(function ($query) use ($end): void {
                    $query->whereNull('effective_from')->orWhereDate('effective_from', '<=', $end->toDateString());
                })
                ->get()
                ->keyBy('user_id');

            $period = PayrollPeriod::create([
                'period_number' => $this->nextNumber('payroll_period', 'PAY', $branch, $start),
                'business_id' => $branch->business_id,
                'branch_id' => $branch->id,
                'name' => $data['name'],
                'start_date' => $start->toDateString(),
                'end_date' => $end->toDateString(),
                'pay_date' => $payDate->toDateString(),
                'status' => 'draft',
                'created_by' => $creator->id,
            ]);

            foreach ($employees as $employee) {
                $compensation = $compensations->get($employee->id);
                $components = [];
                foreach (self::COMPONENTS as $component) {
                    $components[$component] = $compensation ? (string) $compensation->{$component} : '0.00';
                }
                $totals = $this->calculate($components, $employee->name);
                PayrollEntry::create($components + $totals + [
                    'payroll_period_id' => $period->id,
                    'branch_id' => $branch->id,
                    'user_id' => $employee->id,
                    'payslip_number' => $this->nextNumber('payslip', 'PSL', $branch, $start),
                    'employee_number_snapshot' => $employee->employee_number,
                    'employee_name_snapshot' => $employee->name,
                    'position_snapshot' => $employee->position,
                    'status' => 'pending',
                ]);
            }

            return $this->recalculatePeriod($period)->fresh(['branch', 'entries']);
        });
    }

    public function updateEntry(PayrollEntry $entry, array $data, User $actor): PayrollEntry
    {
        $period = PayrollPeriod::findOrFail($entry->payroll_period_id);
        Gate::forUser($actor)->authorize('updateEntries', $period);

        return DB::transaction(function () use ($entry, $data): PayrollEntry {
            $locked = PayrollEntry::query()->lockForUpdate()->findOrFail($entry->id);
            $period = PayrollPeriod::query()->lockForUpdate()->findOrFail($locked->payroll_period_id);
            if ($period->status !== 'draft') {
                throw ValidationException::withMessages(['payroll' => 'Payroll components can only be edited while the period is in draft status.']);
            }

            $components = $this->normalizedComponents($data);
            $totals = $this->calculate($components, $locked->employee_name_snapshot);
            $locked->update($components + $totals);
            $this->recalculatePeriod($period);

            return $locked->fresh(['employee', 'period']);
        });
    }

    public function approve(PayrollPeriod $period, User $approver, ?string $comments = null): PayrollPeriod
    {
        Gate::forUser($approver)->authorize('decide', $period);

        return DB::transaction(function () use ($period, $approver, $comments): PayrollPeriod {
            $locked = PayrollPeriod::query()->with('branch.business')->lockForUpdate()->findOrFail($period->id);
            if ($locked->status !== 'draft') {
                throw ValidationException::withMessages(['payroll' => 'Only a draft payroll period can be approved.']);
            }
            if ($locked->branch->business->maker_checker_enabled && $locked->created_by === $approver->id) {
                throw ValidationException::withMessages(['payroll' => 'Maker-checker is enabled. The employee who created this payroll period cannot approve it.']);
            }
            if (! PayrollEntry::where('payroll_period_id', $locked->id)->exists()) {
                throw ValidationException::withMessages(['payroll' => 'A payroll period cannot be approved without employee entries.']);
            }

            $locked->update([
                'status' => 'approved',
                'approved_by' => $approver->id,
                'approved_at' => now(),
                'decision_comments' => $comments,
                'rejected_by' => null,
                'rejected_at' => null,
            ]);

            return $locked->fresh(['branch', 'entries', 'approver']);
        });
    }

    public function reject(PayrollPeriod $period, User $rejecter, string $comments): PayrollPeriod
    {
        Gate::forUser($rejecter)->authorize('decide', $period);

        return DB::transaction(function () use ($period, $rejecter, $comments): PayrollPeriod {
            $locked = PayrollPeriod::query()->lockForUpdate()->findOrFail($period->id);
            if ($locked->status !== 'draft') {
                throw ValidationException::withMessages(['payroll' => 'Only a draft payroll period can be rejected.']);
            }
            $locked->update([
                'status' => 'rejected',
                'rejected_by' => $rejecter->id,
                'rejected_at' => now(),
                'decision_comments' => $comments,
            ]);
            return $locked->fresh(['branch', 'entries', 'rejecter']);
        });
    }

    public function pay(PayrollPeriod $period, FinancialAccount $selectedAccount, User $payer): PayrollPeriod
    {
        Gate::forUser($payer)->authorize('pay', $period);

        return DB::transaction(function () use ($period, $selectedAccount, $payer): PayrollPeriod {
            $locked = PayrollPeriod::query()->with('branch.business')->lockForUpdate()->findOrFail($period->id);
            if ($locked->status !== 'approved') {
                throw ValidationException::withMessages(['payroll' => 'Only an approved, unpaid payroll period can be paid.']);
            }
            if ($locked->branch->business->maker_checker_enabled && (int) $locked->created_by === (int) $payer->id) {
                throw ValidationException::withMessages(['payroll' => 'Maker-checker is enabled. The employee who prepared this payroll period cannot pay it.']);
            }

            $entries = PayrollEntry::query()
                ->where('payroll_period_id', $locked->id)
                ->orderBy('id')
                ->lockForUpdate()
                ->get();
            if ($entries->contains(fn (PayrollEntry $entry) => $entry->status === 'paid' || $entry->paid_at !== null)) {
                throw ValidationException::withMessages(['payroll' => 'This payroll period contains an entry that has already been paid.']);
            }

            $account = FinancialAccount::query()->lockForUpdate()->findOrFail($selectedAccount->id);
            if ($account->branch_id !== $locked->branch_id) {
                throw ValidationException::withMessages(['financial_account_id' => 'The payroll payment account must belong to the payroll branch.']);
            }
            if ($account->status !== 'active') {
                throw ValidationException::withMessages(['financial_account_id' => 'The payroll payment account must be active.']);
            }

            $totalNetMinor = $entries->sum(fn (PayrollEntry $entry) => $this->money->toMinor((string) $entry->net));
            $balanceMinor = $this->money->toMinor((string) $account->current_balance);
            if ($balanceMinor < $totalNetMinor) {
                throw ValidationException::withMessages(['financial_account_id' => 'The selected account has insufficient funds to pay the full approved payroll.']);
            }

            $runningMinor = $balanceMinor;
            $paymentDate = CarbonImmutable::now();
            foreach ($entries as $entry) {
                $netMinor = $this->money->toMinor((string) $entry->net);
                $transaction = null;
                $journal = null;

                if ($netMinor > 0) {
                    $runningMinor -= $netMinor;
                    $net = $this->money->fromMinor($netMinor);
                    $description = 'Payroll '.$locked->period_number.' — '.$entry->employee_name_snapshot;
                    $transaction = FinancialAccountTransaction::create([
                        'transaction_number' => $this->nextNumber('financial_transaction', 'FTX', $locked->branch, $paymentDate),
                        'account_id' => $account->id,
                        'branch_id' => $locked->branch_id,
                        'transaction_date' => $paymentDate,
                        'reference' => $entry->payslip_number,
                        'type' => 'payroll',
                        'description' => $description,
                        'money_in' => '0.00',
                        'money_out' => $net,
                        'running_balance' => $this->money->fromMinor($runningMinor),
                        'user_id' => $payer->id,
                        'related_type' => PayrollEntry::class,
                        'related_id' => $entry->id,
                    ]);

                    $journal = 'JRN-'.$entry->payslip_number;
                    AccountingEntry::create([
                        'journal_number' => $journal,
                        'branch_id' => $locked->branch_id,
                        'financial_account_id' => $account->id,
                        'entry_date' => $paymentDate,
                        'reference' => $entry->payslip_number,
                        'category' => 'assets',
                        'event_type' => 'payroll_payment',
                        'description' => $description,
                        'debit' => '0.00',
                        'credit' => $net,
                        'user_id' => $payer->id,
                        'related_type' => PayrollEntry::class,
                        'related_id' => $entry->id,
                    ]);
                    AccountingEntry::create([
                        'journal_number' => $journal,
                        'branch_id' => $locked->branch_id,
                        'financial_account_id' => null,
                        'entry_date' => $paymentDate,
                        'reference' => $entry->payslip_number,
                        'category' => 'payroll',
                        'event_type' => 'payroll_payment',
                        'description' => 'Net payroll expense — '.$entry->employee_name_snapshot,
                        'debit' => $net,
                        'credit' => '0.00',
                        'user_id' => $payer->id,
                        'related_type' => PayrollEntry::class,
                        'related_id' => $entry->id,
                    ]);
                }

                $entry->update([
                    'status' => 'paid',
                    'financial_account_id' => $account->id,
                    'financial_account_transaction_id' => $transaction?->id,
                    'accounting_journal_number' => $journal,
                    'paid_by' => $payer->id,
                    'paid_at' => now(),
                ]);
            }

            $account->update([
                'current_balance' => $this->money->fromMinor($runningMinor),
                'updated_by' => $payer->id,
            ]);
            $locked->update([
                'status' => 'paid',
                'paid_from_account_id' => $account->id,
                'paid_by' => $payer->id,
                'paid_at' => now(),
            ]);

            return $locked->fresh(['branch', 'entries', 'paidFromAccount', 'payer']);
        });
    }

    private function normalizedComponents(array $data): array
    {
        $normalized = [];
        foreach (self::COMPONENTS as $component) {
            $minor = $this->money->toMinor((string) ($data[$component] ?? '0'));
            if ($minor < 0) {
                throw ValidationException::withMessages([$component => str($component)->replace('_', ' ')->title().' cannot be negative.']);
            }
            $normalized[$component] = $this->money->fromMinor($minor);
        }
        return $normalized;
    }

    private function calculate(array $components, string $employeeName): array
    {
        $grossMinor = 0;
        foreach (['basic_salary', 'allowances', 'bonus', 'commission', 'overtime'] as $field) {
            $grossMinor += $this->money->toMinor((string) $components[$field]);
        }
        $deductionMinor = 0;
        foreach (['deductions', 'advances', 'other_deductions'] as $field) {
            $deductionMinor += $this->money->toMinor((string) $components[$field]);
        }
        $netMinor = $grossMinor - $deductionMinor;
        if ($netMinor < 0) {
            throw ValidationException::withMessages(['deductions' => 'Total deductions cannot exceed gross pay for '.$employeeName.'.']);
        }

        return [
            'gross' => $this->money->fromMinor($grossMinor),
            'total_deductions' => $this->money->fromMinor($deductionMinor),
            'net' => $this->money->fromMinor($netMinor),
        ];
    }

    private function recalculatePeriod(PayrollPeriod $period): PayrollPeriod
    {
        $gross = $deductions = $net = 0;
        foreach (PayrollEntry::where('payroll_period_id', $period->id)->get(['gross', 'total_deductions', 'net']) as $entry) {
            $gross += $this->money->toMinor((string) $entry->gross);
            $deductions += $this->money->toMinor((string) $entry->total_deductions);
            $net += $this->money->toMinor((string) $entry->net);
        }
        $period->update([
            'total_gross' => $this->money->fromMinor($gross),
            'total_deductions' => $this->money->fromMinor($deductions),
            'total_net' => $this->money->fromMinor($net),
        ]);
        return $period;
    }

    private function nextNumber(string $type, string $prefix, Branch $branch, CarbonInterface $date): string
    {
        $year = (int) $date->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-', '', $branch->code ?? '')) ?: (string) $branch->id);
        return $prefix.'-'.$tag.'-'.$year.'-'.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }
}
