<?php

namespace App\Models;

use App\Casts\TrimmedDecimal;
use App\Concerns\RecordsCreator;
use App\Support\NumberFormat;
use App\Support\SalaryMonth;
use App\Support\StaffAdvanceQuantityMode;
use App\Support\StaffAdvanceType;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'permanent_employee_id',
        'type',
        'advance_date',
        'amount',
        'factory_product_id',
        'quantity_mode',
        'quantity',
        'total_weight',
        'weight_price',
        'notes',
        'is_active',
    ];

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'advance_date' => 'date',
            'amount' => TrimmedDecimal::class.':2',
            'factory_product_id' => 'integer',
            'quantity' => TrimmedDecimal::class.':3',
            'total_weight' => TrimmedDecimal::class.':3',
            'weight_price' => TrimmedDecimal::class.':2',
            'is_active' => 'boolean',
        ];
    }

    public function isActive(): bool
    {
        return $this->is_active;
    }

    public function isCash(): bool
    {
        return $this->type === StaffAdvanceType::CASH;
    }

    public function isProduct(): bool
    {
        return $this->type === StaffAdvanceType::PRODUCT;
    }

    public static function calculateProductAmount(
        string $quantityMode,
        float $quantity,
        float $totalWeight,
        float $unitPrice,
    ): float {
        if (StaffAdvanceQuantityMode::isPack($quantityMode)) {
            return round($quantity * $unitPrice, 2);
        }

        return round($totalWeight * $unitPrice, 2);
    }

    public static function totalAdvanced(int $permanentEmployeeId): float
    {
        return round((float) self::query()
            ->where('permanent_employee_id', $permanentEmployeeId)
            ->where('is_active', true)
            ->sum('amount'), 2);
    }

    public static function totalDeducted(int $permanentEmployeeId, ?int $excludeSalaryPaymentId = null): float
    {
        $query = PermanentEmployeeSalaryPayment::query()
            ->where('permanent_employee_id', $permanentEmployeeId);

        if ($excludeSalaryPaymentId !== null) {
            $query->where('id', '!=', $excludeSalaryPaymentId);
        }

        return round((float) $query->sum('advance_deduction'), 2);
    }

    public static function outstandingBalance(int $permanentEmployeeId, ?int $excludeSalaryPaymentId = null): float
    {
        return max(
            0,
            round(
                self::totalAdvanced($permanentEmployeeId) - self::totalDeducted($permanentEmployeeId, $excludeSalaryPaymentId),
                2,
            ),
        );
    }

    /**
     * @param  list<int>  $permanentEmployeeIds
     * @return array<int, float>
     */
    public static function outstandingBalancesForEmployees(array $permanentEmployeeIds): array
    {
        if ($permanentEmployeeIds === []) {
            return [];
        }

        $advanced = self::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('is_active', true)
            ->selectRaw('permanent_employee_id, COALESCE(SUM(amount), 0) as total_advanced')
            ->groupBy('permanent_employee_id')
            ->pluck('total_advanced', 'permanent_employee_id');

        $deducted = PermanentEmployeeSalaryPayment::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->selectRaw('permanent_employee_id, COALESCE(SUM(advance_deduction), 0) as total_deducted')
            ->groupBy('permanent_employee_id')
            ->pluck('total_deducted', 'permanent_employee_id');

        $balances = [];

        foreach ($permanentEmployeeIds as $employeeId) {
            $balances[$employeeId] = max(
                0,
                round((float) ($advanced[$employeeId] ?? 0) - (float) ($deducted[$employeeId] ?? 0), 2),
            );
        }

        return $balances;
    }

    /**
     * @param  list<int>  $permanentEmployeeIds
     * @return array<int, array{
     *     overall_advances: float,
     *     paid: float,
     *     remaining: float,
     *     types: list<string>,
     *     advance_count: int,
     * }>
     */
    public static function staffSummariesForEmployees(array $permanentEmployeeIds): array
    {
        if ($permanentEmployeeIds === []) {
            return [];
        }

        $advanced = self::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('is_active', true)
            ->selectRaw('permanent_employee_id, COALESCE(SUM(amount), 0) as total_advanced, COUNT(*) as advance_count')
            ->groupBy('permanent_employee_id')
            ->get()
            ->keyBy('permanent_employee_id');

        $deducted = PermanentEmployeeSalaryPayment::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->selectRaw('permanent_employee_id, COALESCE(SUM(advance_deduction), 0) as total_deducted')
            ->groupBy('permanent_employee_id')
            ->pluck('total_deducted', 'permanent_employee_id');

        $types = self::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('is_active', true)
            ->get(['permanent_employee_id', 'type'])
            ->groupBy('permanent_employee_id')
            ->map(fn ($rows) => $rows->pluck('type')->unique()->values()->all());

        $summaries = [];

        foreach ($permanentEmployeeIds as $employeeId) {
            $overall = round((float) ($advanced[$employeeId]->total_advanced ?? 0), 2);
            $paid = round((float) ($deducted[$employeeId] ?? 0), 2);

            $summaries[$employeeId] = [
                'overall_advances' => $overall,
                'paid' => $paid,
                'remaining' => max(0, round($overall - $paid, 2)),
                'types' => $types[$employeeId] ?? [],
                'advance_count' => (int) ($advanced[$employeeId]->advance_count ?? 0),
            ];
        }

        return $summaries;
    }

    public static function formatAdvanceNumber(int $advanceId): string
    {
        return str_pad((string) $advanceId, 4, '0', STR_PAD_LEFT);
    }

    /**
     * FIFO allocation of each salary deduction onto active advances (oldest first).
     *
     * @param  list<int>  $permanentEmployeeIds
     * @return array<int, array<int, list<array{
     *     salary_month: string,
     *     salary_month_label: string,
     *     amount: string,
     *     status: string,
     * }>>>
     */
    public static function deductionTransactionsForEmployees(array $permanentEmployeeIds): array
    {
        if ($permanentEmployeeIds === []) {
            return [];
        }

        $transactions = array_fill_keys($permanentEmployeeIds, []);

        $advancesByEmployee = self::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('is_active', true)
            ->orderBy('advance_date')
            ->orderBy('id')
            ->get(['id', 'permanent_employee_id', 'amount'])
            ->groupBy('permanent_employee_id');

        $paymentsByEmployee = PermanentEmployeeSalaryPayment::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('advance_deduction', '>', 0)
            ->orderBy('salary_month')
            ->orderBy('id')
            ->get(['id', 'permanent_employee_id', 'salary_month', 'advance_deduction', 'status'])
            ->groupBy('permanent_employee_id');

        foreach ($permanentEmployeeIds as $employeeId) {
            $remainingByAdvance = [];

            foreach ($advancesByEmployee->get($employeeId, collect()) as $advance) {
                $remainingByAdvance[$advance->id] = round((float) $advance->amount, 2);
                $transactions[$employeeId][$advance->id] = [];
            }

            foreach ($paymentsByEmployee->get($employeeId, collect()) as $payment) {
                $remainingPayment = round((float) $payment->advance_deduction, 2);

                foreach ($remainingByAdvance as $advanceId => $remainingAdvance) {
                    if ($remainingPayment <= 0) {
                        break;
                    }

                    if ($remainingAdvance <= 0) {
                        continue;
                    }

                    $applied = round(min($remainingAdvance, $remainingPayment), 2);
                    $remainingByAdvance[$advanceId] = round($remainingAdvance - $applied, 2);
                    $remainingPayment = round($remainingPayment - $applied, 2);

                    $transactions[$employeeId][$advanceId][] = [
                        'salary_month' => $payment->salary_month?->format('Y-m-d') ?? '',
                        'salary_month_label' => $payment->salary_month !== null
                            ? SalaryMonth::displayLabel(Carbon::parse($payment->salary_month))
                            : '',
                        'amount' => NumberFormat::money($applied),
                        'advance_deduction' => NumberFormat::money($applied),
                        'status' => $payment->status,
                    ];
                }
            }
        }

        return $transactions;
    }

    /**
     * FIFO allocation of salary deductions across active advances (oldest first).
     *
     * @param  list<int>  $permanentEmployeeIds
     * @return array<int, array<int, array{paid_amount: float, unpaid_amount: float, settlement_status: string}>>
     */
    public static function settlementAllocationsForEmployees(array $permanentEmployeeIds): array
    {
        if ($permanentEmployeeIds === []) {
            return [];
        }

        $allocations = array_fill_keys($permanentEmployeeIds, []);

        $advancesByEmployee = self::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('is_active', true)
            ->orderBy('advance_date')
            ->orderBy('id')
            ->get(['id', 'permanent_employee_id', 'amount'])
            ->groupBy('permanent_employee_id');

        $deductedByEmployee = PermanentEmployeeSalaryPayment::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->selectRaw('permanent_employee_id, COALESCE(SUM(advance_deduction), 0) as total_deducted')
            ->groupBy('permanent_employee_id')
            ->pluck('total_deducted', 'permanent_employee_id');

        foreach ($permanentEmployeeIds as $employeeId) {
            $remainingDeduction = round((float) ($deductedByEmployee[$employeeId] ?? 0), 2);

            foreach ($advancesByEmployee->get($employeeId, collect()) as $advance) {
                $amount = (float) $advance->amount;
                $paid = round(min($amount, $remainingDeduction), 2);
                $remainingDeduction = round(max(0, $remainingDeduction - $paid), 2);
                $unpaid = round($amount - $paid, 2);

                $allocations[$employeeId][$advance->id] = [
                    'paid_amount' => $paid,
                    'unpaid_amount' => $unpaid,
                    'settlement_status' => self::settlementStatus($paid, $unpaid),
                ];
            }
        }

        return $allocations;
    }

    /**
     * @param  list<int>  $permanentEmployeeIds
     * @return array<int, list<array{
     *     salary_month: string,
     *     salary_month_label: string,
     *     advance_deduction: string,
     *     status: string,
     * }>>
     */
    public static function deductionHistoriesForEmployees(array $permanentEmployeeIds): array
    {
        if ($permanentEmployeeIds === []) {
            return [];
        }

        $histories = array_fill_keys($permanentEmployeeIds, []);

        PermanentEmployeeSalaryPayment::query()
            ->whereIn('permanent_employee_id', $permanentEmployeeIds)
            ->where('advance_deduction', '>', 0)
            ->orderBy('salary_month')
            ->orderBy('id')
            ->get(['permanent_employee_id', 'salary_month', 'advance_deduction', 'status'])
            ->each(function (PermanentEmployeeSalaryPayment $payment) use (&$histories): void {
                $histories[$payment->permanent_employee_id][] = [
                    'salary_month' => $payment->salary_month?->format('Y-m-d') ?? '',
                    'salary_month_label' => $payment->salary_month !== null
                        ? SalaryMonth::displayLabel(Carbon::parse($payment->salary_month))
                        : '',
                    'advance_deduction' => (string) $payment->advance_deduction,
                    'status' => $payment->status,
                ];
            });

        return $histories;
    }

    /**
     * @return array{paid_amount: float, unpaid_amount: float, settlement_status: string}
     */
    public static function inactiveSettlement(): array
    {
        return [
            'paid_amount' => 0.0,
            'unpaid_amount' => 0.0,
            'settlement_status' => 'inactive',
        ];
    }

    private static function settlementStatus(float $paid, float $unpaid): string
    {
        if ($unpaid <= 0) {
            return 'paid';
        }

        if ($paid <= 0) {
            return 'unpaid';
        }

        return 'partial';
    }

    /**
     * @return array{paid_amount: string, unpaid_amount: string, settlement_status: string}
     */
    public static function formatSettlement(array $settlement): array
    {
        return [
            'paid_amount' => NumberFormat::money($settlement['paid_amount']),
            'unpaid_amount' => NumberFormat::money($settlement['unpaid_amount']),
            'settlement_status' => $settlement['settlement_status'],
        ];
    }

    /**
     * @return list<array{
     *     id: int,
     *     type: string,
     *     advance_date: string,
     *     amount: string,
     *     product_name: string|null,
     *     quantity_mode: string|null,
     *     quantity: string|null,
     *     total_weight: string|null,
     *     weight_price: string|null,
     * }>
     */
    public static function itemsForEmployee(int $permanentEmployeeId): array
    {
        return self::query()
            ->where('permanent_employee_id', $permanentEmployeeId)
            ->where('is_active', true)
            ->with('factoryProduct:id,name')
            ->orderBy('advance_date')
            ->orderBy('id')
            ->get()
            ->map(fn (self $advance): array => [
                'id' => $advance->id,
                'type' => $advance->type,
                'advance_date' => $advance->advance_date?->format('Y-m-d') ?? '',
                'amount' => (string) $advance->amount,
                'product_name' => $advance->factoryProduct?->name,
                'quantity_mode' => $advance->isProduct()
                    ? StaffAdvanceQuantityMode::normalize($advance->quantity_mode)
                    : null,
                'quantity' => $advance->quantity !== null ? (string) $advance->quantity : null,
                'total_weight' => $advance->total_weight !== null ? (string) $advance->total_weight : null,
                'weight_price' => $advance->weight_price !== null ? (string) $advance->weight_price : null,
            ])
            ->values()
            ->all();
    }

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

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