<?php

namespace App\Domain\HR\Services;

use App\Domain\HR\Enums\AdvanceStatus;
use App\Domain\HR\Enums\CompensationStatus;
use App\Domain\HR\Enums\PayrollPeriodStatus;
use App\Domain\HR\Enums\PayslipItemType;
use App\Domain\HR\Enums\PayslipStatus;
use App\Domain\HR\Models\PayrollPeriod;
use App\Domain\HR\Models\Payslip;
use App\Domain\HR\Models\PayslipItem;
use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerAdvance;
use App\Domain\HR\Models\TrainerCompensation;
use App\Domain\Financial\Enums\TransactionType;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Transaction;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Services\SettingsService;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class PayrollService
{
    public function __construct(
        private CompensationCalculatorService $compensationCalculator,
        private SettingsService $settings,
    ) {}

    /**
     * Create or get the current open payroll period.
     */
    public function getOrCreateCurrentPeriod(int $academyId, User $actor): PayrollPeriod
    {
        $now = now();
        $payrollDay = (int) $this->settings->get('payroll_day', 28);

        // Period runs from payroll_day of previous month to (payroll_day - 1) of current month
        if ($now->day >= $payrollDay) {
            $periodStart = $now->copy()->day($payrollDay);
            $periodEnd   = $now->copy()->addMonth()->day($payrollDay)->subDay();
        } else {
            $periodStart = $now->copy()->subMonth()->day($payrollDay);
            $periodEnd   = $now->copy()->day($payrollDay)->subDay();
        }

        $existing = PayrollPeriod::where('academy_id', $academyId)
            ->where('period_start', $periodStart->toDateString())
            ->first();

        if ($existing) {
            return $existing;
        }

        return PayrollPeriod::create([
            'academy_id'   => $academyId,
            'period_start' => $periodStart->toDateString(),
            'period_end'   => $periodEnd->toDateString(),
            'status'       => PayrollPeriodStatus::Open,
            'created_by'   => $actor->id,
        ]);
    }

    /**
     * Calculate payslips for all active trainers in a period.
     * This is the big monthly operation.
     */
    public function calculatePeriod(PayrollPeriod $period, User $actor): PayrollPeriod
    {
        return DB::transaction(function () use ($period, $actor) {
            if (!(bool) $this->settings->get('payroll_enabled', false)) {
                throw new DomainException('نظام الرواتب غير مفعل — يرجى تفعيله من إعدادات النظام');
            }

            $period->update(['status' => PayrollPeriodStatus::Calculating]);

            $academyId   = $period->academy_id;
            $periodStart = Carbon::parse($period->period_start);
            $periodEnd   = Carbon::parse($period->period_end);

            // Get all active trainers — every branch's. `trainers` is shared, so
            // the scope keeps the academy-wide ones but drops any trainer pinned
            // to another branch, and a payroll run that skips people is a payroll
            // run nobody notices is wrong until they are not paid.
            $trainers = Trainer::withoutBranchScope()
                ->where('academy_id', $academyId)
                ->where('status', 'active')
                ->get();

            // Calculate monthly items (player pay, revenue share) for each trainer
            foreach ($trainers as $trainer) {
                $this->compensationCalculator->calculatePlayerPay($trainer, $periodStart, $periodEnd, $actor);
                $this->compensationCalculator->calculateRevenueShare($trainer, $periodStart, $periodEnd, $actor);
            }

            $totalGross      = 0;
            $totalDeductions = 0;
            $totalNet        = 0;
            $payslipCount    = 0;

            foreach ($trainers as $trainer) {
                $payslip = $this->generatePayslip($trainer, $period, $periodStart, $periodEnd, $actor);
                if ($payslip) {
                    $totalGross      += $payslip->gross_amount;
                    $totalDeductions += $payslip->total_deductions;
                    $totalNet        += $payslip->net_amount;
                    $payslipCount++;
                }
            }

            $period->update([
                'status'           => PayrollPeriodStatus::Review,
                'total_gross'      => $totalGross,
                'total_deductions' => $totalDeductions,
                'total_net'        => $totalNet,
                'payslip_count'    => $payslipCount,
            ]);

            return $period->fresh();
        });
    }

    /**
     * Generate a single trainer's payslip for a period.
     */
    public function generatePayslip(
        Trainer $trainer,
        PayrollPeriod $period,
        Carbon $periodStart,
        Carbon $periodEnd,
        User $actor,
    ): ?Payslip {
        // Payroll is academy-wide: a trainer has one contract and one payslip
        // per period, covering every branch they coached in. Each cross-branch
        // read below is deliberate and narrow — left scoped, a payslip
        // calculated from branch A would silently omit the sessions its trainer
        // worked at branch B, and the amount would depend on which branch the
        // clerk had selected. (The existing rows were backfilled onto the main
        // branch, so from any other branch the scope sees none of them at all.)
        //
        // Get all pending/approved compensation records for this trainer in this period
        $compensations = TrainerCompensation::withoutBranchScope()
            ->where('trainer_id', $trainer->id)
            ->whereBetween('date', [$periodStart->toDateString(), $periodEnd->toDateString()])
            ->whereIn('status', [CompensationStatus::Pending->value, CompensationStatus::Approved->value])
            ->get();

        // Get base salary (if salaried/hybrid)
        $baseSalary = 0;
        if (in_array($trainer->compensation_model->value, ['salary', 'hybrid'])) {
            $employee = $trainer->employee;
            if ($employee && $employee->salary_amount) {
                $baseSalary = $employee->salary_amount;
            }
        }

        // Skip if nothing to pay
        $hasCompensation = $compensations->isNotEmpty() || $baseSalary > 0;
        if (!$hasCompensation) {
            return null;
        }

        // Delete existing draft payslip for this trainer + period (recalculation).
        // Across branches for the same reason: a draft left in another branch
        // would survive the recalculation and pay the trainer twice.
        Payslip::withoutBranchScope()
            ->where('trainer_id', $trainer->id)
            ->where('payroll_period_id', $period->id)
            ->where('status', PayslipStatus::Draft->value)
            ->delete();

        // Separate earnings from penalties
        $sessionEarnings = $compensations->filter(fn ($c) => $c->type->isEarning())->sum('amount');
        $penalties       = $compensations->filter(fn ($c) => !$c->type->isEarning())->sum('amount');

        // Advance deduction
        $advanceDeduction = $this->calculateAdvanceDeduction($trainer);

        // Insurance — Egypt: employee share applied to base salary only
        $insuranceEnabled = (bool) $this->settings->get('social_insurance_enabled', false);
        $insurancePercent = $insuranceEnabled ? (float) $this->settings->get('social_insurance_percent', 0) : 0;
        $insuranceAmount  = 0;
        if ($baseSalary > 0 && $insurancePercent > 0) {
            $insuranceAmount = (int) round($baseSalary * $insurancePercent / 100);
        }

        // Tax — simplified: Egyptian tax bracket implementation pending
        $taxAmount = 0; // TODO: implement Egyptian tax brackets

        $grossAmount      = $baseSalary + $sessionEarnings;
        $bonuses          = 0; // Manual bonuses are added separately
        $otherDeductions  = 0;
        $totalDeductions  = $penalties + $advanceDeduction + $taxAmount + $insuranceAmount + $otherDeductions;
        $netAmount        = max(0, $grossAmount + $bonuses - $totalDeductions);

        // Generate payslip number
        $payslipNumber = $this->generatePayslipNumber($period->academy_id, $periodEnd);

        $payslip = Payslip::create([
            'academy_id'        => $period->academy_id,
            'trainer_id'        => $trainer->id,
            'payroll_period_id' => $period->id,
            'payslip_number'    => $payslipNumber,
            'base_amount'       => $baseSalary,
            'session_earnings'  => $sessionEarnings,
            'bonuses'           => $bonuses,
            'penalties'         => $penalties,
            'advances_deducted' => $advanceDeduction,
            'tax_amount'        => $taxAmount,
            'insurance_amount'  => $insuranceAmount,
            'other_deductions'  => $otherDeductions,
            'gross_amount'      => $grossAmount,
            'total_deductions'  => $totalDeductions,
            'net_amount'        => $netAmount,
            'status'            => PayslipStatus::Draft,
            'created_by'        => $actor->id,
        ]);

        // ─── Line Items ──────────────────────────────────────────────

        $sortOrder = 0;

        // Base salary line
        if ($baseSalary > 0) {
            PayslipItem::create([
                'payslip_id'  => $payslip->id,
                'type'        => PayslipItemType::BaseSalary,
                'description' => 'الراتب الأساسي',
                'quantity'    => 1,
                'rate'        => $baseSalary,
                'amount'      => $baseSalary,
                'is_deduction' => false,
                'sort_order'  => $sortOrder++,
            ]);
        }

        // Group compensation records by type and create one line item per type
        $grouped = $compensations->groupBy(fn ($c) => $c->type->value);
        foreach ($grouped as $typeValue => $items) {
            $itemType    = $this->mapCompTypeToPayslipType($typeValue);
            $isDeduction = $itemType->isDeduction();
            $totalAmount = $items->sum('amount');
            $count       = $items->count();
            $avgRate     = $count > 0 ? (int) round($totalAmount / $count) : 0;

            PayslipItem::create([
                'payslip_id'  => $payslip->id,
                'type'        => $itemType,
                'description' => $items->first()->type->label() . " ({$count})",
                'quantity'    => $count,
                'rate'        => $avgRate,
                'amount'      => $isDeduction ? -$totalAmount : $totalAmount,
                'is_deduction' => $isDeduction,
                'sort_order'  => $sortOrder++,
            ]);
        }

        // Advance deduction line
        if ($advanceDeduction > 0) {
            PayslipItem::create([
                'payslip_id'  => $payslip->id,
                'type'        => PayslipItemType::AdvanceDeduction,
                'description' => 'قسط سلفة',
                'quantity'    => 1,
                'rate'        => $advanceDeduction,
                'amount'      => -$advanceDeduction,
                'is_deduction' => true,
                'sort_order'  => $sortOrder++,
            ]);
        }

        // Insurance line
        if ($insuranceAmount > 0) {
            PayslipItem::create([
                'payslip_id'  => $payslip->id,
                'type'        => PayslipItemType::Insurance,
                'description' => "تأمينات اجتماعية ({$insurancePercent}%)",
                'quantity'    => 1,
                'rate'        => $insuranceAmount,
                'amount'      => -$insuranceAmount,
                'is_deduction' => true,
                'sort_order'  => $sortOrder++,
            ]);
        }

        return $payslip;
    }

    /**
     * Approve a single payslip.
     */
    public function approvePayslip(Payslip $payslip, User $actor): Payslip
    {
        if (
            $payslip->status !== PayslipStatus::Draft &&
            $payslip->status !== PayslipStatus::PendingApproval
        ) {
            throw new DomainException('لا يمكن الموافقة على هذا الكشف في حالته الحالية');
        }

        return DB::transaction(function () use ($payslip, $actor) {
            $payslip->update([
                'status'      => PayslipStatus::Approved,
                'approved_by' => $actor->id,
                'approved_at' => now(),
            ]);

            // Mark all related compensation records as approved — every branch's,
            // because the payslip being approved was calculated from all of them.
            TrainerCompensation::withoutBranchScope()
                ->where('trainer_id', $payslip->trainer_id)
                ->whereBetween('date', [
                    $payslip->period->period_start,
                    $payslip->period->period_end,
                ])
                ->where('status', CompensationStatus::Pending->value)
                ->update([
                    'status'      => CompensationStatus::Approved,
                    'approved_by' => $actor->id,
                    'approved_at' => now(),
                ]);

            return $payslip->fresh();
        });
    }

    /**
     * Mark a payslip as paid and create the financial transaction.
     */
    public function markPaid(
        Payslip $payslip,
        string $paymentMethod,
        ?string $paymentReference,
        User $actor,
    ): Payslip {
        if ($payslip->status !== PayslipStatus::Approved) {
            throw new DomainException('يجب الموافقة على الكشف قبل تسجيل الدفع');
        }

        return DB::transaction(function () use ($payslip, $paymentMethod, $paymentReference, $actor) {
            $payslip->update([
                'status'            => PayslipStatus::Paid,
                'paid_at'           => now(),
                'payment_method'    => $paymentMethod,
                'payment_reference' => $paymentReference,
            ]);

            // Mark related compensation records as paid — every branch's, to
            // match what the payslip actually paid for.
            TrainerCompensation::withoutBranchScope()
                ->where('trainer_id', $payslip->trainer_id)
                ->whereBetween('date', [
                    $payslip->period->period_start,
                    $payslip->period->period_end,
                ])
                ->whereIn('status', [CompensationStatus::Pending->value, CompensationStatus::Approved->value])
                ->update(['status' => CompensationStatus::Paid]);

            // Deduct outstanding advance installment
            if ($payslip->advances_deducted > 0) {
                $this->applyAdvanceDeduction($payslip->trainer_id, $payslip->advances_deducted);
            }

            // Double-entry financial transaction
            $this->createPayslipTransaction($payslip, $paymentMethod, $actor);

            return $payslip->fresh();
        });
    }

    /**
     * Approve an entire payroll period (moves all draft payslips to pending_approval).
     */
    public function approvePeriod(PayrollPeriod $period, User $actor): PayrollPeriod
    {
        if ($period->status !== PayrollPeriodStatus::Review) {
            throw new DomainException('فترة الرواتب ليست في مرحلة المراجعة');
        }

        return DB::transaction(function () use ($period, $actor) {
            // Move all draft payslips to pending_approval — the period covers
            // the whole academy, so approving it from one branch must not leave
            // another branch's payslips behind in draft.
            Payslip::withoutBranchScope()
                ->where('payroll_period_id', $period->id)
                ->where('status', PayslipStatus::Draft->value)
                ->update(['status' => PayslipStatus::PendingApproval->value]);

            $period->update([
                'status'      => PayrollPeriodStatus::Approved,
                'approved_by' => $actor->id,
                'approved_at' => now(),
            ]);

            return $period->fresh();
        });
    }

    /**
     * Close a payroll period. All payslips must be paid first.
     */
    public function closePeriod(PayrollPeriod $period, User $actor): PayrollPeriod
    {
        // Across branches: closing a period while another branch still holds an
        // unpaid payslip is exactly what this guard exists to prevent.
        $unpaid = Payslip::withoutBranchScope()
            ->where('payroll_period_id', $period->id)
            ->whereNotIn('status', [PayslipStatus::Paid->value, PayslipStatus::Cancelled->value])
            ->count();

        if ($unpaid > 0) {
            throw new DomainException("لا يمكن إغلاق الفترة — يوجد {$unpaid} كشف غير مدفوع");
        }

        return DB::transaction(function () use ($period, $actor) {
            $period->update([
                'status'    => PayrollPeriodStatus::Closed,
                'closed_at' => now(),
            ]);

            return $period->fresh();
        });
    }

    // ─── Private Helpers ────────────────────────────────────────────

    /**
     * Calculate how much to deduct from the trainer's active advance this cycle.
     */
    private function calculateAdvanceDeduction(Trainer $trainer): int
    {
        // A trainer's advance is owed to the academy, not to a branch: reading
        // it scoped would stop deducting the instalment the moment payroll ran
        // from a different branch than the one the advance was issued at.
        $activeAdvance = TrainerAdvance::withoutBranchScope()
            ->where('trainer_id', $trainer->id)
            ->where('status', AdvanceStatus::Active->value)
            ->first();

        if (!$activeAdvance) {
            return 0;
        }

        // Deduct the installment amount, but never more than the remaining balance
        return min($activeAdvance->installment_amount, $activeAdvance->remaining_balance);
    }

    /**
     * Apply a deduction against the trainer's active advance and update its state.
     */
    private function applyAdvanceDeduction(int $trainerId, int $amount): void
    {
        // Same advance, same reason (see calculateAdvanceDeduction): the
        // deduction and the balance it is taken from must find the same row.
        $advance = TrainerAdvance::withoutBranchScope()
            ->where('trainer_id', $trainerId)
            ->where('status', AdvanceStatus::Active->value)
            ->first();

        if (!$advance) {
            return;
        }

        $newBalance      = max(0, $advance->remaining_balance - $amount);
        $newInstallments = $advance->installments_paid + 1;

        $advance->update([
            'remaining_balance' => $newBalance,
            'installments_paid' => $newInstallments,
            'status'            => $newBalance <= 0 ? AdvanceStatus::FullyDeducted : AdvanceStatus::Active,
        ]);
    }

    /**
     * Generate a unique payslip number in the format PS{ym}{seq}.
     */
    private function generatePayslipNumber(int $academyId, Carbon $date): string
    {
        $prefix = 'PS';
        $month  = $date->format('ym');

        // The payslip number is unique per academy, so it has to be counted per
        // academy: counting only this branch's payslips hands the same number
        // to one trainer in each branch.
        $count = Payslip::withoutBranchScope()
            ->where('academy_id', $academyId)
            ->where('payslip_number', 'like', "{$prefix}{$month}%")
            ->count();

        $seq = str_pad($count + 1, 4, '0', STR_PAD_LEFT);

        return "{$prefix}{$month}{$seq}";
    }

    /**
     * Create the double-entry financial transaction for a paid payslip.
     * Debit: Salary Expense (5000)
     * Credit: Cash (1000) or Bank (1010) based on payment method
     */
    private function createPayslipTransaction(Payslip $payslip, string $paymentMethod, User $actor): void
    {
        $academyId = $payslip->academy_id;

        // Resolve salary expense account (code 5000)
        $salaryExpenseAccount = FinancialAccount::where('academy_id', $academyId)
            ->where('code', '5000')
            ->first();

        if (!$salaryExpenseAccount) {
            throw new DomainException('حساب مصروف الرواتب غير موجود — يرجى إعداد شجرة الحسابات');
        }

        // Resolve cash/bank account based on payment method
        $creditAccountCode = match ($paymentMethod) {
            'cash' => '1000',
            'bank_transfer', 'instapay', 'cheque' => '1010',
            default => '1000',
        };

        $creditAccount = FinancialAccount::where('academy_id', $academyId)
            ->where('code', $creditAccountCode)
            ->first();

        if (!$creditAccount) {
            throw new DomainException('حساب النقدية/البنك غير موجود — يرجى إعداد شجرة الحسابات');
        }

        Transaction::create([
            'academy_id' => $academyId,
            'debit_account_id' => $salaryExpenseAccount->id,
            'credit_account_id' => $creditAccount->id,
            'reference_type' => Payslip::class,
            'reference_id' => $payslip->id,
            'amount' => $payslip->net_amount,
            'currency' => 'EGP',
            'type' => TransactionType::PaymentMade,
            'description' => "راتب: {$payslip->payslip_number}",
            'transaction_date' => now()->toDateString(),
            'created_by' => $actor->id,
        ]);
    }

    /**
     * Map a CompensationType value to the corresponding PayslipItemType.
     */
    private function mapCompTypeToPayslipType(string $compType): PayslipItemType
    {
        return match ($compType) {
            'session_pay'       => PayslipItemType::SessionPay,
            'group_pay'         => PayslipItemType::GroupPay,
            'player_pay'        => PayslipItemType::PlayerPay,
            'revenue_share'     => PayslipItemType::RevenueShare,
            'bonus'             => PayslipItemType::Bonus,
            'penalty'           => PayslipItemType::Penalty,
            'overtime'          => PayslipItemType::Overtime,
            'substitute'        => PayslipItemType::Substitute,
            'cancelled_session' => PayslipItemType::CancelledSessionPay,
            default             => PayslipItemType::SessionPay,
        };
    }
}
