<?php

namespace App\Models;

use App\Casts\TrimmedDecimal;
use App\Concerns\RecordsCreator;
use App\Support\PermanentEmployeeMonthlyAttendance;
use App\Support\SalaryMonth;
use App\Support\SalaryPaymentStatus;
use App\Support\StaffType;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;

class PermanentEmployeeSalaryPayment extends Model implements Auditable
{
    use AuditableTrait, RecordsCreator;

    public const PAYMENT_METHOD_FINGERPRINT = 'fingerprint';

    public const PAYMENT_METHOD_MANUAL = 'manual';

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'deduct_absent_days' => true,
        'advance_deduction' => 0,
    ];

    /**
     * @var list<string>
     */
    protected $auditExclude = [
        'fingerprint_template',
    ];

    /**
     * @var list<string>
     */
    protected $fillable = [
        'permanent_employee_id',
        'salary_month',
        'base_salary',
        'allowance',
        'salary_increment',
        'absent_days',
        'absent_deduction',
        'deduct_absent_days',
        'advance_deduction',
        'amount',
        'status',
        'payment_method',
        'fingerprint_template',
        'fingerprint_captured_at',
        'paid_at',
        'paid_by',
    ];

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'salary_month' => 'date:Y-m-d',
            'base_salary' => TrimmedDecimal::class.':2',
            'allowance' => TrimmedDecimal::class.':2',
            'salary_increment' => TrimmedDecimal::class.':2',
            'absent_days' => 'integer',
            'absent_deduction' => TrimmedDecimal::class.':2',
            'deduct_absent_days' => 'boolean',
            'advance_deduction' => TrimmedDecimal::class.':2',
            'amount' => TrimmedDecimal::class.':2',
            'fingerprint_template' => 'encrypted',
            'fingerprint_captured_at' => 'datetime',
            'paid_at' => 'datetime',
        ];
    }

    public function isDraft(): bool
    {
        return $this->status === SalaryPaymentStatus::DRAFT;
    }

    public function isPaid(): bool
    {
        return $this->status === SalaryPaymentStatus::PAID;
    }

    public function punishmentTotalForSalaryMonth(): float
    {
        if ($this->salary_month === null) {
            return 0.0;
        }

        return PermanentEmployeePunishment::totalForMonth(
            $this->permanent_employee_id,
            Carbon::parse($this->salary_month),
        );
    }

    public function recalculateAmount(): self
    {
        $absentDeduction = $this->deduct_absent_days ? (float) $this->absent_deduction : 0.0;

        $this->amount = max(
            0,
            (float) $this->base_salary
                + (float) $this->allowance
                + (float) $this->salary_increment
                - $absentDeduction
                - $this->punishmentTotalForSalaryMonth()
                - (float) $this->advance_deduction,
        );

        return $this;
    }

    /**
     * Gross pay before and after the monthly increment line (base + allowance [+ increment]).
     *
     * @return array{salary_before_increment: string, salary_after_increment: string}|null
     */
    public function incrementSalaryDisplay(): ?array
    {
        $increment = (float) $this->salary_increment;

        if ($increment <= 0) {
            return null;
        }

        if ($this->isDraft() && $this->salary_month !== null) {
            $figures = $this->figuresFromContract();
            $before = round($figures['base_salary'] + (float) $this->allowance, 2);
        } else {
            $before = round((float) $this->base_salary + (float) $this->allowance, 2);
        }

        return [
            'salary_before_increment' => (string) $before,
            'salary_after_increment' => (string) round($before + $increment, 2),
        ];
    }

    /**
     * Salary breakdown from the employee's current contract and attendance for this month.
     *
     * @return array{
     *     base_salary: float,
     *     absent_days: int,
     *     absent_deduction: float,
     *     punishment_total: float,
     *     punishment_dates: list<string>,
     *     advance_deduction: float,
     *     outstanding_advance_balance: float,
     *     advance_items: list<array<string, mixed>>,
     *     amount: float,
     *     employed_days: int,
     *     days_in_month: int,
     *     is_prorated: bool,
     * }
     */
    public function salaryMonthAnchor(): ?string
    {
        if ($this->salary_month === null) {
            return null;
        }

        return SalaryMonth::anchorForQuery($this->salary_month);
    }

    public function figuresFromContract(): array
    {
        $this->loadMissing('permanentEmployee');

        $employee = $this->permanentEmployee;
        $salaryMonthAnchor = $this->salaryMonthAnchor() ?? SalaryMonth::normalizeStart($this->salary_month);

        $proration = PermanentEmployeeMonthlyAttendance::proratedBaseSalary($employee, $salaryMonthAnchor);
        $absentDays = PermanentEmployeeMonthlyAttendance::absentDayCount($employee, $salaryMonthAnchor);
        $absentDeduction = 0.0;

        if ($this->deduct_absent_days && $absentDays > 0) {
            $absentDeduction = round(
                PermanentEmployeeMonthlyAttendance::absentDeductionDailyRate($employee, $salaryMonthAnchor)
                    * $absentDays,
                2,
            );
        }

        $punishmentSummary = PermanentEmployeePunishment::summaryForSalaryMonth($employee->id, $salaryMonthAnchor);
        $advanceDeduction = (float) $this->advance_deduction;
        $outstandingAdvanceBalance = PermanentEmployeeAdvance::outstandingBalance($employee->id, $this->id);

        $amount = max(
            0,
            $proration['base_salary']
                + (float) $this->allowance
                + (float) $this->salary_increment
                - ($this->deduct_absent_days ? $absentDeduction : 0.0)
                - $punishmentSummary['total']
                - $advanceDeduction,
        );

        return [
            'base_salary' => $proration['base_salary'],
            'absent_days' => $absentDays,
            'absent_deduction' => $absentDeduction,
            'punishment_total' => $punishmentSummary['total'],
            'punishment_dates' => $punishmentSummary['effective_dates'],
            'advance_deduction' => $advanceDeduction,
            'outstanding_advance_balance' => $outstandingAdvanceBalance,
            'advance_items' => PermanentEmployeeAdvance::itemsForEmployee($employee->id),
            'amount' => $amount,
            'employed_days' => $proration['employed_days'],
            'days_in_month' => $proration['days_in_month'],
            'is_prorated' => $proration['is_prorated'],
        ];
    }

    public function refreshDraftFromContract(): bool
    {
        if (! $this->isDraft() || $this->salary_month === null) {
            return false;
        }

        // Figures depend on deduct_absent_days; compute after enabling deduction.
        $this->deduct_absent_days = true;

        $figures = $this->figuresFromContract();

        $this->forceFill([
            'base_salary' => $figures['base_salary'],
            'absent_days' => $figures['absent_days'],
            'absent_deduction' => $figures['absent_deduction'],
            'deduct_absent_days' => true,
            'amount' => $figures['amount'],
        ])->save();

        return true;
    }

    public static function refreshDraftsForSalaryMonth(CarbonInterface|string $salaryMonth): int
    {
        $monthKey = SalaryMonth::urlKey(SalaryMonth::normalizeStart($salaryMonth));

        $refreshed = 0;

        self::query()
            ->forSalaryMonth($monthKey)
            ->where('status', SalaryPaymentStatus::DRAFT)
            ->with('permanentEmployee')
            ->get()
            ->each(function (self $payment) use (&$refreshed): void {
                if ($payment->refreshDraftFromContract()) {
                    $refreshed++;
                }
            });

        return $refreshed;
    }

    public static function refreshDraftsForEmployee(PermanentEmployee $employee): void
    {
        $employee->salaryPayments()
            ->where('status', SalaryPaymentStatus::DRAFT)
            ->get()
            ->each(fn (self $payment) => $payment->refreshDraftFromContract());
    }

    public static function refreshDraftsForAttendance(
        string $staffType,
        int $staffId,
        CarbonInterface|string|null $punchedAt = null,
    ): void {
        if ($staffType !== StaffType::PERMANENT) {
            return;
        }

        $employee = PermanentEmployee::query()->find($staffId);

        if ($employee === null) {
            return;
        }

        self::refreshDraftsForEmployee($employee);

        if ($punchedAt !== null) {
            self::refreshDraftsForSalaryMonth(SalaryMonth::normalizeStart($punchedAt));
        }
    }

    /**
     * @param  Builder<self>  $query
     */
    public function scopeForSalaryMonth(Builder $query, CarbonInterface|string $salaryMonth): Builder
    {
        return $query->where('salary_month', SalaryMonth::anchorForQuery($salaryMonth));
    }

    public function wasPaidWithFingerprint(): bool
    {
        return $this->payment_method === self::PAYMENT_METHOD_FINGERPRINT;
    }

    public function wasPaidManually(): bool
    {
        return $this->payment_method === self::PAYMENT_METHOD_MANUAL;
    }

    /**
     * @return BelongsTo<PermanentEmployee, $this>
     */
    public function permanentEmployee(): BelongsTo
    {
        return $this->belongsTo(PermanentEmployee::class);
    }

    /**
     * @return BelongsTo<User, $this>
     */
    public function paidByUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'paid_by');
    }
}
