<?php

namespace App\Http\Controllers;

use App\Models\CashModal;
use App\Models\CashMutation;
use App\Models\Deposit;
use App\Models\Employee;
use App\Models\Payment;
use App\Models\PaymentAccount;
use App\Models\PurchasePayment;
use App\Models\Salary;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class SalaryController extends Controller
{
    private const AUTO_DEPOSIT_TAG = '[AUTO-DEPOSIT]';

    public function index(Request $request)
    {
    $selectedPeriod = $request->filled('period')
        ? $request->period
        : now()->format('Y-m');

    $query = Salary::with('employee')
        ->where('period', $selectedPeriod);

    if ($request->filled('employee_id')) {
        $query->where('employee_id', $request->employee_id);
    }

    if ($request->filled('status')) {
        $query->where('status', $request->status);
    }

    /*
     * Dibuat tetap memakai paginate supaya bagian blade:
     * {{ $salaries->links() }}
     * tidak error.
     *
     * Bedanya, jumlah per halaman dibuat sesuai total data bulan ini,
     * jadi semua karyawan/gaji di bulan berjalan tampil semua.
     */
    $totalRows = (clone $query)->count();

    $salaries = $query
        ->latest()
        ->paginate($totalRows > 0 ? $totalRows : 1)
        ->withQueryString();

    $employees = Employee::where('status', 'active')->orderBy('name')->get();
    $paymentAccounts = PaymentAccount::where('is_active', 1)->orderBy('name')->get();

    $periods = [];
    for ($i = 0; $i < 12; $i++) {
        $date = now()->subMonths($i);
        $periods[$date->format('Y-m')] = $date->translatedFormat('F Y');
    }

    return view('pages.salaries.index', [
        'title' => 'Gaji Karyawan',
        'salaries' => $salaries,
        'employees' => $employees,
        'paymentAccounts' => $paymentAccounts,
        'periods' => $periods,
        'statuses' => Salary::STATUSES,
        'selectedPeriod' => $selectedPeriod,
    ]);
    }

    public function create()
    {
        $employees = Employee::where('status', 'active')->orderBy('name')->get();

        $periods = [];
        for ($i = -1; $i <= 2; $i++) {
            $date = now()->addMonths($i);
            $periods[$date->format('Y-m')] = $date->translatedFormat('F Y');
        }

        return view('pages.salaries.form', [
            'title' => 'Input Gaji Karyawan',
            'salary' => new Salary(),
            'employees' => $employees,
            'periods' => $periods,
        ]);
    }

    public function store(Request $request)
    {
        $request->merge([
            'base_salary' => str_replace('.', '', $request->base_salary ?? '0'),
            'allowance' => str_replace('.', '', $request->allowance ?? '0'),
            'overtime' => str_replace('.', '', $request->overtime ?? '0'),
            'bonus' => str_replace('.', '', $request->bonus ?? '0'),
            'deduction' => str_replace('.', '', $request->deduction ?? '0'),
        ]);

        $validated = $request->validate([
            'employee_id' => 'required|exists:employees,id',
            'period' => 'required|string|regex:/^\d{4}-\d{2}$/',
            'base_salary' => 'nullable|numeric|min:0',
            'allowance' => 'nullable|numeric|min:0',
            'overtime' => 'nullable|numeric|min:0',
            'bonus' => 'nullable|numeric|min:0',
            'deduction' => 'nullable|numeric|min:0',
            'notes' => 'nullable|string|max:500',
        ]);

        if (
            Salary::where('employee_id', $validated['employee_id'])
                ->where('period', $validated['period'])
                ->exists()
        ) {
            return redirect()->back()
                ->withInput()
                ->with('error', 'Gaji untuk karyawan dan periode ini sudah ada.');
        }

        $employee = Employee::findOrFail($validated['employee_id']);

        $validated['base_salary'] = (float) ($employee->base_salary ?? 0);
        $validated['allowance'] = $validated['allowance'] ?? 0;
        $validated['overtime'] = $validated['overtime'] ?? 0;
        $validated['bonus'] = $validated['bonus'] ?? 0;
        $validated['deduction'] = $validated['deduction'] ?? 0;

        Salary::create($validated);

        return redirect()->route('salaries.index')
            ->with('success', 'Data gaji berhasil ditambahkan.');
    }

    public function edit(Salary $salary)
    {
        $employees = Employee::where('status', 'active')->orderBy('name')->get();

        $periods = [];
        for ($i = -6; $i <= 2; $i++) {
            $date = now()->addMonths($i);
            $periods[$date->format('Y-m')] = $date->translatedFormat('F Y');
        }

        return view('pages.salaries.form', [
            'title' => 'Edit Gaji Karyawan',
            'salary' => $salary,
            'employees' => $employees,
            'periods' => $periods,
        ]);
    }

    public function update(Request $request, Salary $salary)
    {
        $request->merge([
            'base_salary' => str_replace('.', '', $request->base_salary ?? '0'),
            'allowance' => str_replace('.', '', $request->allowance ?? '0'),
            'overtime' => str_replace('.', '', $request->overtime ?? '0'),
            'bonus' => str_replace('.', '', $request->bonus ?? '0'),
            'deduction' => str_replace('.', '', $request->deduction ?? '0'),
        ]);

        $validated = $request->validate([
            'employee_id' => 'required|exists:employees,id',
            'period' => 'required|string|regex:/^\d{4}-\d{2}$/',
            'base_salary' => 'nullable|numeric|min:0',
            'allowance' => 'nullable|numeric|min:0',
            'overtime' => 'nullable|numeric|min:0',
            'bonus' => 'nullable|numeric|min:0',
            'deduction' => 'nullable|numeric|min:0',
            'notes' => 'nullable|string|max:500',
        ]);

        if (
            Salary::where('employee_id', $validated['employee_id'])
                ->where('period', $validated['period'])
                ->where('id', '!=', $salary->id)
                ->exists()
        ) {
            return redirect()->back()
                ->withInput()
                ->with('error', 'Gaji untuk karyawan dan periode ini sudah ada.');
        }

        // Gaji yang sudah dibayar tetap boleh direvisi.
        // Saldo kas akan menyesuaikan karena perhitungan saldo mengambil nilai terbaru dari tabel salaries.
        $employee = Employee::findOrFail($validated['employee_id']);

        $validated['base_salary'] = (float) ($employee->base_salary ?? 0);
        $validated['allowance'] = $validated['allowance'] ?? 0;
        $validated['overtime'] = $validated['overtime'] ?? 0;
        $validated['bonus'] = $validated['bonus'] ?? 0;
        $validated['deduction'] = $validated['deduction'] ?? 0;

        $salary->update($validated);

        return redirect()->route('salaries.index')
            ->with('success', 'Data gaji berhasil diperbarui.');
    }

    public function destroy(Salary $salary)
    {
        $salary->delete();

        return redirect()->route('salaries.index')
            ->with('success', 'Data gaji berhasil dihapus. Jika data sudah dibayar, saldo akan menyesuaikan ulang berdasarkan data gaji yang tersisa.');
    }

    public function markPaid(Request $request, Salary $salary)
    {
        $validated = $request->validate([
            'paid_at' => ['required', 'date'],
            'payment_source' => ['required', 'in:cash,bank'],
            'payment_account_id' => ['nullable', 'integer', 'exists:payment_accounts,id'],
            'payment_notes' => ['nullable', 'string', 'max:1000'],
        ]);

        if ($salary->status === 'paid') {
            return redirect()->route('salaries.index')
                ->with('error', 'Gaji ini sudah ditandai sebagai dibayar.');
        }

        if ($validated['payment_source'] === 'bank' && empty($validated['payment_account_id'])) {
            return redirect()->route('salaries.index')
                ->with('error', 'Rekening bank wajib dipilih saat pembayaran dari Kas Bank.');
        }

        if ($validated['payment_source'] === 'cash') {
            $validated['payment_account_id'] = null;
        }

        $totalSalary = $this->getSalaryNetAmount($salary);

        if ($totalSalary <= 0) {
            return redirect()->route('salaries.index')
                ->with('error', 'Total gaji tidak valid.');
        }

        $paidAt = Carbon::parse($validated['paid_at'])->endOfDay();

        $availableBalance = $this->getAvailableBalanceForSalarySource(
            $validated['payment_source'],
            $paidAt,
            $salary->id
        );

        if ($availableBalance < $totalSalary) {
            return redirect()->route('salaries.index')
                ->with(
                    'error',
                    'Saldo ' . ($validated['payment_source'] === 'cash' ? 'Kas Tunai' : 'Kas Bank') .
                    ' tidak cukup. Saldo tersedia: Rp ' . number_format($availableBalance, 0, ',', '.') .
                    ', total gaji: Rp ' . number_format($totalSalary, 0, ',', '.')
                );
        }

        DB::transaction(function () use ($salary, $validated, $paidAt) {
            $updateData = [
                'status' => 'paid',
            ];

            if (Schema::hasColumn('salaries', 'paid_at')) {
                $updateData['paid_at'] = $paidAt;
            }

            if (Schema::hasColumn('salaries', 'payment_method')) {
                $updateData['payment_method'] = $validated['payment_source'] === 'cash' ? 'cash' : 'transfer';
            }

            if (Schema::hasColumn('salaries', 'payment_source')) {
                $updateData['payment_source'] = $validated['payment_source'];
            }

            if (Schema::hasColumn('salaries', 'payment_account_id')) {
                $updateData['payment_account_id'] = $validated['payment_account_id'] ?? null;
            }

            if (Schema::hasColumn('salaries', 'payment_notes')) {
                $updateData['payment_notes'] = $validated['payment_notes'] ?? null;
            }

            if (Schema::hasColumn('salaries', 'paid_by')) {
                $updateData['paid_by'] = auth()->id();
            }

            $salary->update($updateData);
        });

        return redirect()->route('salaries.index')
            ->with('success', 'Gaji berhasil ditandai sebagai dibayar.');
    }

    public function slip(Salary $salary)
    {
        $salary->load('employee');

        $settings = [
            'company_name' => \App\Models\Setting::getShopName(),
            'company_address' => \App\Models\Setting::getShopAddress(),
            'company_phone' => \App\Models\Setting::getShopPhone(),
            'company_logo' => \App\Models\Setting::getShopLogo(),
        ];

        return view('pages.salaries.slip', [
            'title' => 'Slip Gaji',
            'salary' => $salary,
            'settings' => $settings,
            'shopLogo' => $settings['company_logo'],
        ]);
    }

    private function getSalaryNetAmount(Salary $salary): float
    {
        return
            (float) ($salary->base_salary ?? 0) +
            (float) ($salary->allowance ?? 0) +
            (float) ($salary->overtime ?? 0) +
            (float) ($salary->bonus ?? 0) -
            (float) ($salary->deduction ?? 0);
    }

    private function getAvailableBalanceForSalarySource(string $source, Carbon $paidAt, ?int $excludeSalaryId = null): float
    {
        $paymentDateField = $this->pickDateField('payments', ['payment_date', 'paid_at', 'created_at']);
        $purchasePayDateField = Schema::hasTable('purchase_payments')
            ? $this->pickDateField('purchase_payments', ['payment_date', 'created_at'])
            : null;
        $depositDateField = Schema::hasTable('deposits')
            ? $this->pickDateField('deposits', ['deposit_date', 'created_at'])
            : null;
        $modalDateField = Schema::hasTable('cash_modals')
            ? $this->pickDateField('cash_modals', ['modal_date', 'created_at'])
            : null;
        $mutationDateField = Schema::hasTable('cash_mutations')
            ? $this->pickDateField('cash_mutations', ['mutation_date', 'created_at'])
            : null;

        $paymentsHasAccount = Schema::hasColumn('payments', 'payment_account_id');
        $paymentsHasMethod = Schema::hasColumn('payments', 'payment_method');
        $purchasePaysHasAccount = Schema::hasTable('purchase_payments') && Schema::hasColumn('purchase_payments', 'payment_account_id');
        $purchasePaysHasMethod = Schema::hasTable('purchase_payments') && Schema::hasColumn('purchase_payments', 'payment_method');

        if ($source === 'cash') {
            return
                $this->sumPaymentsByBucket('cash', null, $paidAt, $paymentDateField, $paymentsHasAccount, $paymentsHasMethod) +
                $this->sumCashModalByAccount('cash', null, $paidAt, $modalDateField) +
                $this->sumNetMutation('cash', null, $paidAt, $mutationDateField, true) -
                $this->sumDeposits(null, $paidAt, $depositDateField) -
                $this->sumPurchasePaymentsByBucket('cash', null, $paidAt, $purchasePayDateField, $purchasePaysHasAccount, $purchasePaysHasMethod) -
                $this->sumPaidSalariesBySource('cash', null, $paidAt, $excludeSalaryId);
        }

        return
            $this->sumPaymentsByBucket('bank', null, $paidAt, $paymentDateField, $paymentsHasAccount, $paymentsHasMethod) +
            $this->sumDeposits(null, $paidAt, $depositDateField) +
            $this->sumCashModalByAccount('bank', null, $paidAt, $modalDateField) +
            $this->sumNetMutation('bank', null, $paidAt, $mutationDateField, true) -
            $this->sumPurchasePaymentsByBucket('bank', null, $paidAt, $purchasePayDateField, $purchasePaysHasAccount, $purchasePaysHasMethod) -
            $this->sumPaidSalariesBySource('bank', null, $paidAt, $excludeSalaryId);
    }

    private function sumPaymentsByBucket(
        string $bucket,
        ?Carbon $start,
        ?Carbon $end,
        string $paymentDateField,
        bool $hasAccountColumn,
        bool $hasMethodColumn
    ): float {
        $query = Payment::query()->orderBy($paymentDateField);

        $this->applyDateTimeRange($query, $paymentDateField, $start, $end);
        $this->applyActiveOrderFilterToPayments($query);

        if ($bucket === 'cash') {
            if (!$hasAccountColumn && !$hasMethodColumn) {
                return (float) $query->sum('amount');
            }

            $query->where(function ($q) use ($hasAccountColumn, $hasMethodColumn) {
                $added = false;

                if ($hasMethodColumn) {
                    $q->where('payment_method', 'cash');
                    $added = true;
                }

                if ($hasAccountColumn) {
                    if ($added) {
                        $q->orWhereNull('payment_account_id');
                    } else {
                        $q->whereNull('payment_account_id');
                    }
                }
            });

            return (float) $query->sum('amount');
        }

        if (!$hasAccountColumn && !$hasMethodColumn) {
            return (float) $query->sum('amount');
        }

        $query->where(function ($q) use ($hasAccountColumn, $hasMethodColumn) {
            $added = false;

            if ($hasMethodColumn) {
                $q->whereIn('payment_method', ['transfer', 'ewallet']);
                $added = true;
            }

            if ($hasAccountColumn) {
                if ($added) {
                    $q->orWhereNotNull('payment_account_id');
                } else {
                    $q->whereNotNull('payment_account_id');
                }
            }
        });

        return (float) $query->sum('amount');
    }

    private function sumPurchasePaymentsByBucket(
        string $bucket,
        ?Carbon $start,
        ?Carbon $end,
        ?string $purchasePayDateField,
        bool $hasAccountColumn,
        bool $hasMethodColumn
    ): float {
        if (!Schema::hasTable('purchase_payments') || !$purchasePayDateField) {
            return 0.0;
        }

        $query = PurchasePayment::query()->orderBy($purchasePayDateField);
        $this->applyDateTimeRange($query, $purchasePayDateField, $start, $end);

        return (float) $query->get()->filter(function ($item) use ($bucket, $hasAccountColumn, $hasMethodColumn) {
            $method = $this->resolvePurchasePaymentMethod($item, $hasAccountColumn, $hasMethodColumn);

            return $bucket === 'cash'
                ? $method === 'cash'
                : $method === 'transfer';
        })->sum('amount');
    }

    private function resolvePurchasePaymentMethod($item, bool $hasAccountColumn, bool $hasMethodColumn): ?string
    {
        if ($hasMethodColumn && !empty($item->payment_method)) {
            return strtolower(trim((string) $item->payment_method));
        }

        $notes = trim((string) ($item->notes ?? ''));

        if (preg_match('/Metode:\s*Bank Transfer/i', $notes)) {
            return 'transfer';
        }

        if (preg_match('/Metode:\s*Cash/i', $notes)) {
            return 'cash';
        }

        if ($hasAccountColumn && !empty($item->payment_account_id)) {
            return 'transfer';
        }

        return null;
    }

    private function sumCashModalByAccount(string $account, ?Carbon $start, ?Carbon $end, ?string $modalDateField): float
    {
        if (!Schema::hasTable('cash_modals') || !$modalDateField) {
            return 0.0;
        }

        $query = CashModal::query()->where('account', $account);
        $this->applyDateTimeRange($query, $modalDateField, $start, $end);

        return (float) $query->sum('amount');
    }

    private function sumNetMutation(
        string $account,
        ?Carbon $start,
        ?Carbon $end,
        ?string $mutationDateField,
        bool $excludeAutoDeposit = true
    ): float {
        if (!Schema::hasTable('cash_mutations') || !$mutationDateField) {
            return 0.0;
        }

        $query = CashMutation::query();

        if ($excludeAutoDeposit) {
            $query->where(function ($q) {
                $q->whereNull('notes')
                    ->orWhere('notes', 'not like', '%' . self::AUTO_DEPOSIT_TAG . '%');
            });
        }

        $this->applyDateTimeRange($query, $mutationDateField, $start, $end);

        $in = (clone $query)->where('to_account', $account)->sum('amount');
        $out = (clone $query)->where('from_account', $account)->sum('amount');

        return (float) $in - (float) $out;
    }

    private function sumDeposits(?Carbon $start, ?Carbon $end, ?string $depositDateField): float
    {
        if (!Schema::hasTable('deposits') || !$depositDateField) {
            return 0.0;
        }

        $query = Deposit::query();
        $this->applyDateTimeRange($query, $depositDateField, $start, $end);

        return (float) $query->sum('amount');
    }

    private function sumPaidSalariesBySource(
        string $source,
        ?Carbon $start,
        ?Carbon $end,
        ?int $excludeSalaryId = null
    ): float {
        if (!Schema::hasTable('salaries')) {
            return 0.0;
        }

        $dateField = Schema::hasColumn('salaries', 'paid_at')
            ? 'paid_at'
            : (Schema::hasColumn('salaries', 'updated_at') ? 'updated_at' : 'created_at');

        $query = Salary::query()->where('status', 'paid');

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

        if (Schema::hasColumn('salaries', 'payment_source')) {
            $query->where('payment_source', $source);
        } elseif (Schema::hasColumn('salaries', 'payment_method')) {
            $query->where('payment_method', $source === 'cash' ? 'cash' : 'transfer');
        } elseif (Schema::hasColumn('salaries', 'payment_account_id')) {
            if ($source === 'cash') {
                $query->whereNull('payment_account_id');
            } else {
                $query->whereNotNull('payment_account_id');
            }
        } else {
            return 0.0;
        }

        $this->applyDateTimeRange($query, $dateField, $start, $end);

        return (float) $query->get()->sum(function ($salary) {
            return $this->getSalaryNetAmount($salary);
        });
    }

    private function pickDateField(string $table, array $candidates): string
    {
        foreach ($candidates as $column) {
            if (Schema::hasColumn($table, $column)) {
                return $column;
            }
        }

        return 'created_at';
    }

    private function applyActiveOrderFilterToPayments(Builder $query): void
    {
        if (!Schema::hasColumn('payments', 'order_id')) {
            return;
        }

        $query->where(function ($q) {
            $q->whereNull('order_id')
                ->orWhereHas('order', function ($orderQuery) {
                    $orderQuery->where('is_cancelled', false);
                });
        });
    }

    private function applyDateTimeRange($query, string $column, ?Carbon $start, ?Carbon $end): void
    {
        if ($start) {
            $query->where($column, '>=', $start->copy()->startOfDay());
        }

        if ($end) {
            $query->where($column, '<=', $end->copy()->endOfDay());
        }
    }
}
