<?php

namespace App\Services;

use App\Models\{
    Account, AuditLog, FiscalYear, LedgerEntry,
    Setting, TransactionAttachment, Voucher, VoucherApproval, VoucherLine
};
use Illuminate\Support\Facades\DB;

class VoucherPostingService
{
    public const NEGATIVE_CASH_MESSAGE = 'This Transaction will Make the Cash Balance Negative. Please Check all the Information.';

    /**
     * Post a voucher: validate balance, write ledger entries, update status.
     * Must be called inside a DB transaction.
     *
     * @throws \RuntimeException on validation failure
     */
    public static function post(Voucher $voucher, ?string $closingPassword = null): void
    {
        if ($voucher->isPosted()) {
            throw new \RuntimeException('Voucher is already posted.');
        }
        if ($voucher->isCancelled()) {
            throw new \RuntimeException('Cannot post a cancelled voucher.');
        }

        $companyId = $voucher->company_id;
        $lines     = $voucher->lines()->with('account')->get();

        if ($lines->isEmpty()) {
            throw new \RuntimeException('Voucher has no lines. Add at least one debit and one credit line.');
        }

        // ── Validate double-entry balance ────────────────────────────────────
        $totalDebit  = $lines->where('dr_cr', 'dr')->sum('line_total');
        $totalCredit = $lines->where('dr_cr', 'cr')->sum('line_total');

        // Allow 0.01 rounding tolerance
        if (abs($totalDebit - $totalCredit) > 0.01) {
            throw new \RuntimeException(
                sprintf(
                    'Voucher is not balanced. Total Debit: %s, Total Credit: %s, Difference: %s',
                    number_format($totalDebit, 2),
                    number_format($totalCredit, 2),
                    number_format(abs($totalDebit - $totalCredit), 2)
                )
            );
        }

        self::assertNoNegativeCashBalanceForLines($companyId, $lines, $voucher->id);

        // ── Validate fiscal year is open ─────────────────────────────────────
        $fiscalYear = FiscalYear::withoutGlobalScopes()->find($voucher->fiscal_year_id);
        if (!$fiscalYear || $fiscalYear->status === 'locked') {
            throw new \RuntimeException('The fiscal year is locked. No new entries can be posted.');
        }

        app(BooksClosingService::class)->assertCanTransact(
            $companyId,
            $voucher->voucher_date,
            $closingPassword
        );

        // ── Check prior period posting setting ───────────────────────────────
        $allowPrior = Setting::get($companyId, 'allow_prior_period_posting', '0');
        if (!$allowPrior) {
            $currentFY = FiscalYear::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('is_current', true)
                ->first();

            if ($currentFY && $voucher->voucher_date->lt($currentFY->start_date)) {
                throw new \RuntimeException(
                    "Posting to a prior period is not allowed. " .
                    "Current fiscal year starts: {$currentFY->start_date->format('d M Y')}. " .
                    "Enable 'Allow Prior Period Posting' in Accounting Settings to override."
                );
            }
        }

        // ── Check for duplicate voucher number (if already assigned) ─────────
        if ($voucher->voucher_number) {
            $duplicate = Voucher::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('voucher_type', $voucher->voucher_type)
                ->where('voucher_number', $voucher->voucher_number)
                ->where('id', '!=', $voucher->id)
                ->where('status', 'posted')
                ->exists();

            if ($duplicate) {
                throw new \RuntimeException(
                    "Voucher number '{$voucher->voucher_number}' has already been posted. Please use a different number."
                );
            }
        }

        // ── Delete any existing ledger entries (re-posting after edit) ────────
        LedgerEntry::withoutGlobalScopes()
            ->where('voucher_id', $voucher->id)
            ->delete();

        // ── Write ledger entries ─────────────────────────────────────────────
        foreach ($lines as $line) {
            $isDebit = ($line->dr_cr === 'dr');
            $amount  = (float) $line->line_total;

            LedgerEntry::withoutGlobalScopes()->create([
                'company_id'      => $companyId,
                'fiscal_year_id'  => $voucher->fiscal_year_id,
                'account_id'      => $line->account_id,
                'voucher_id'      => $voucher->id,
                'voucher_line_id' => $line->id,
                'entry_date'      => $voucher->voucher_date,
                'debit'           => $isDebit ? $amount : 0,
                'credit'          => $isDebit ? 0 : $amount,
                'reference'       => $voucher->voucher_number,
                'narration'       => $line->description ?? $voucher->narration,
                'party_id'        => $line->party_id ?: $voucher->party_id,
                'party_type'      => $line->party_type ?: $voucher->party_type,
            ]);
        }

        // ── Update voucher status ─────────────────────────────────────────────
        $voucher->update([
            'status'    => 'posted',
            'posted_by' => auth()->id(),
            'posted_at' => now(),
            'grand_total'   => $totalDebit, // = totalCredit when balanced
            'total_amount'  => $totalDebit,
        ]);

        // ── Record approval log entry ─────────────────────────────────────────
        VoucherApproval::withoutGlobalScopes()->create([
            'company_id' => $companyId,
            'voucher_id' => $voucher->id,
            'action'     => 'posted',
            'user_id'    => auth()->id(),
            'comments'   => 'Voucher posted successfully.',
        ]);

        AuditLog::record('post', 'vouchers', (string) $voucher->id,
            ['status' => $voucher->getOriginal('status')],
            ['status' => 'posted', 'voucher_number' => $voucher->voucher_number],
            "Voucher posted: {$voucher->voucher_number}"
        );
    }

    public static function assertNoNegativeCashBalanceForLines(int $companyId, iterable $lines, ?int $excludeVoucherId = null): void
    {
        if (Setting::get($companyId, 'allow_negative_cash_bank', '0') === '1') {
            return;
        }

        $impacts = [];

        foreach ($lines as $line) {
            $accountId = self::lineAccountId($line);
            if (!$accountId) {
                continue;
            }

            $impacts[$accountId] = ($impacts[$accountId] ?? 0) + self::lineDebit($line) - self::lineCredit($line);
        }

        $negativeCashImpacts = collect($impacts)
            ->filter(fn(float $impact) => $impact < -0.009);

        if ($negativeCashImpacts->isEmpty()) {
            return;
        }

        $cashAccounts = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_cash', true)
            ->where('is_bank', false)
            ->whereIn('id', $negativeCashImpacts->keys())
            ->get(['id']);

        foreach ($cashAccounts as $account) {
            $currentBalance = (float) LedgerEntry::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('account_id', $account->id)
                ->when($excludeVoucherId, fn($query) => $query->where('voucher_id', '!=', $excludeVoucherId))
                ->selectRaw('COALESCE(SUM(debit),0) - COALESCE(SUM(credit),0) as balance')
                ->value('balance');

            if ($currentBalance + (float) $negativeCashImpacts[$account->id] < -0.009) {
                throw new \RuntimeException(self::NEGATIVE_CASH_MESSAGE);
            }
        }
    }

    private static function lineAccountId(mixed $line): ?int
    {
        $accountId = is_array($line) ? ($line['account_id'] ?? null) : ($line->account_id ?? null);

        return $accountId ? (int) $accountId : null;
    }

    private static function lineDebit(mixed $line): float
    {
        if (is_array($line)) {
            $debit = (float) ($line['debit'] ?? 0);
            if ($debit > 0) {
                return $debit;
            }

            return ($line['dr_cr'] ?? null) === 'dr' ? (float) ($line['amount'] ?? 0) : 0.0;
        }

        return ($line->dr_cr ?? null) === 'dr' ? (float) ($line->line_total ?? 0) : (float) ($line->debit ?? 0);
    }

    private static function lineCredit(mixed $line): float
    {
        if (is_array($line)) {
            $credit = (float) ($line['credit'] ?? 0);
            if ($credit > 0) {
                return $credit;
            }

            return ($line['dr_cr'] ?? null) === 'cr' ? (float) ($line['amount'] ?? 0) : 0.0;
        }

        return ($line->dr_cr ?? null) === 'cr' ? (float) ($line->line_total ?? 0) : (float) ($line->credit ?? 0);
    }

    /**
     * Reverse a posted voucher — creates a new reversal voucher with opposite Dr/Cr.
     */
    public static function reverse(Voucher $original, string $reason, string $reversalDate = null): Voucher
    {
        if (!$original->isPosted()) {
            throw new \RuntimeException('Only posted vouchers can be reversed.');
        }

        $companyId   = $original->company_id;
        $reversalDate = $reversalDate ?? now()->toDateString();

        app(BooksClosingService::class)->assertCanTransact($companyId, $original->voucher_date);
        app(BooksClosingService::class)->assertCanTransact($companyId, $reversalDate);

        $fiscalYear = app(FiscalYearService::class)->requireOpenForDate($companyId, $reversalDate);

        $reversalNumber = VoucherNumberService::generate(
            $companyId,
            $original->voucher_type,
            new \DateTime($reversalDate)
        );

        $reversal = Voucher::withoutGlobalScopes()->create([
            'company_id'     => $companyId,
            'fiscal_year_id' => $fiscalYear->id,
            'voucher_type'   => $original->voucher_type,
            'voucher_number' => $reversalNumber,
            'voucher_date'   => $reversalDate,
            'reference'      => "REV-{$original->voucher_number}",
            'currency_id'    => $original->currency_id,
            'exchange_rate'  => $original->exchange_rate,
            'narration'      => "Reversal of {$original->voucher_number}: {$reason}",
            'status'         => 'draft',
            'created_by'     => auth()->id(),
            'extra_data'     => ['reversed_from' => $original->id],
        ]);

        // Copy lines with swapped Dr/Cr
        $lineNum = 1;
        foreach ($original->lines as $line) {
            VoucherLine::withoutGlobalScopes()->create([
                'company_id'       => $companyId,
                'voucher_id'       => $reversal->id,
                'line_number'      => $lineNum++,
                'account_id'       => $line->account_id,
                'description'      => "Reversal: " . ($line->description ?? ''),
                'quantity'         => $line->quantity,
                'uom_id'           => $line->uom_id,
                'unit_price'       => $line->unit_price,
                'line_total'       => $line->line_total,
                'dr_cr'            => $line->dr_cr === 'dr' ? 'cr' : 'dr', // SWAP
                'tax_code_id'      => $line->tax_code_id,
                'tax_rate'         => $line->tax_rate,
                'tax_amount'       => $line->tax_amount,
                'discount_percent' => $line->discount_percent,
                'discount_amount'  => $line->discount_amount,
                'party_id'         => $line->party_id,
                'party_type'       => $line->party_type,
                'dim1_id'          => $line->dim1_id,
                'dim2_id'          => $line->dim2_id,
                'dim3_id'          => $line->dim3_id,
                'dim4_id'          => $line->dim4_id,
                'dim5_id'          => $line->dim5_id,
                'dim6_id'          => $line->dim6_id,
                'dim7_id'          => $line->dim7_id,
            ]);
        }

        // FIX: Update reversal voucher totals after creating lines
        $reversalLines = $reversal->lines;
        $revTotal = $reversalLines->sum('line_total');
        $reversal->update([
            'total_amount' => $revTotal,
            'grand_total'  => $revTotal,
        ]);

        // Mark original as reversed in extra_data
        $extraData = $original->extra_data ?? [];
        $extraData['reversed_by'] = $reversal->id;
        $original->update(['extra_data' => $extraData]);

        AuditLog::record('reverse', 'vouchers', (string) $original->id,
            ['status' => 'posted'],
            ['reversed_by' => $reversal->id, 'reason' => $reason],
            "Voucher reversed: {$original->voucher_number} → {$reversalNumber}"
        );

        return $reversal;
    }

    /**
     * Cancel a draft/submitted voucher (not posted — use reverse() for posted).
     */
    public static function cancel(Voucher $voucher, string $reason): void
    {
        if ($voucher->isPosted()) {
            throw new \RuntimeException('Posted vouchers cannot be cancelled. Use Reverse instead.');
        }
        if ($voucher->isCancelled()) {
            throw new \RuntimeException('Voucher is already cancelled.');
        }

        $voucher->update([
            'status'           => 'cancelled',
            'cancelled_by'     => auth()->id(),
            'cancelled_at'     => now(),
            'rejection_reason' => $reason,
        ]);

        VoucherApproval::withoutGlobalScopes()->create([
            'company_id' => $voucher->company_id,
            'voucher_id' => $voucher->id,
            'action'     => 'cancelled',
            'user_id'    => auth()->id(),
            'comments'   => $reason,
        ]);

        AuditLog::record('cancel', 'vouchers', (string) $voucher->id,
            ['status' => $voucher->getOriginal('status')],
            ['status' => 'cancelled', 'reason' => $reason],
            "Voucher cancelled: {$voucher->voucher_number}"
        );
    }

    /**
     * Submit a draft voucher for approval.
     */
    public static function submit(Voucher $voucher): void
    {
        if (!$voucher->isDraft()) {
            throw new \RuntimeException('Only draft vouchers can be submitted.');
        }

        $lines = $voucher->lines()->count();
        if ($lines === 0) {
            throw new \RuntimeException('Cannot submit a voucher with no lines.');
        }

        $voucher->update([
            'status'       => 'submitted',
            'submitted_by' => auth()->id(),
            'submitted_at' => now(),
        ]);

        VoucherApproval::withoutGlobalScopes()->create([
            'company_id' => $voucher->company_id,
            'voucher_id' => $voucher->id,
            'action'     => 'submitted',
            'user_id'    => auth()->id(),
            'comments'   => 'Submitted for approval.',
        ]);

        AuditLog::record('submit', 'vouchers', (string) $voucher->id,
            ['status' => 'draft'], ['status' => 'submitted'],
            "Voucher submitted: {$voucher->voucher_number}"
        );
    }

    /**
     * Approve and immediately post a draft/submitted voucher.
     */
    public static function approve(Voucher $voucher, string $comments = ''): void
    {
        if (!$voucher->isDraft() && !$voucher->isSubmitted()) {
            throw new \RuntimeException('Only draft or submitted vouchers can be approved.');
        }

        $oldStatus = $voucher->status;

        $voucher->update([
            'status'      => 'approved',
            'submitted_by'=> $voucher->submitted_by ?: auth()->id(),
            'submitted_at'=> $voucher->submitted_at ?: now(),
            'approved_by' => auth()->id(),
            'approved_at' => now(),
        ]);

        VoucherApproval::withoutGlobalScopes()->create([
            'company_id' => $voucher->company_id,
            'voucher_id' => $voucher->id,
            'action'     => 'approved',
            'user_id'    => auth()->id(),
            'comments'   => $comments ?: 'Approved and posted to ledger.',
        ]);

        AuditLog::record('approve', 'vouchers', (string) $voucher->id,
            ['status' => $oldStatus], ['status' => 'approved'],
            "Voucher approved: {$voucher->voucher_number}"
        );

        self::post($voucher->fresh(['lines']));
    }

    /**
     * Reject a submitted voucher back to draft.
     */
    public static function reject(Voucher $voucher, string $reason): void
    {
        if (!$voucher->isSubmitted()) {
            throw new \RuntimeException('Only submitted vouchers can be rejected.');
        }

        $voucher->update([
            'status'           => 'draft',
            'rejection_reason' => $reason,
        ]);

        VoucherApproval::withoutGlobalScopes()->create([
            'company_id' => $voucher->company_id,
            'voucher_id' => $voucher->id,
            'action'     => 'rejected',
            'user_id'    => auth()->id(),
            'comments'   => $reason,
        ]);

        AuditLog::record('reject', 'vouchers', (string) $voucher->id,
            ['status' => 'submitted'], ['status' => 'draft', 'reason' => $reason],
            "Voucher rejected: {$voucher->voucher_number}"
        );
    }

    /**
     * Calculate totals from lines and update voucher header.
     */
    public static function recalculateTotals(Voucher $voucher): void
    {
        $lines = $voucher->lines;

        $totalDebit  = $lines->where('dr_cr', 'dr')->sum('line_total');
        $totalCredit = $lines->where('dr_cr', 'cr')->sum('line_total');
        $totalTax    = $lines->sum('tax_amount');
        $totalDisc   = $lines->sum('discount_amount');

        $voucher->update([
            'total_amount'   => max($totalDebit, $totalCredit),
            'total_tax'      => $totalTax,
            'total_discount' => $totalDisc,
            'grand_total'    => max($totalDebit, $totalCredit),
        ]);
    }

    /**
     * Permanently remove a voucher and all related ledger/line records.
     * Use for admin cleanup; posted vouchers are removed from the ledger.
     */
    public static function hardDelete(Voucher $voucher): void
    {
        if ($voucher->isPosted()) {
            app(BooksClosingService::class)->assertCanTransact(
                $voucher->company_id,
                $voucher->voucher_date
            );
        }

        $extraData = $voucher->extra_data ?? [];
        if (!empty($extraData['reversed_by'])) {
            throw new \RuntimeException('This voucher has been reversed. Delete the reversal voucher first.');
        }

        if (!empty($extraData['reversed_from'])) {
            $original = Voucher::withoutGlobalScopes()->find($extraData['reversed_from']);
            if ($original) {
                $origExtra = $original->extra_data ?? [];
                unset($origExtra['reversed_by']);
                $original->update(['extra_data' => $origExtra]);
            }
        }

        $number = $voucher->voucher_number;
        $voucherId = $voucher->id;

        LedgerEntry::withoutGlobalScopes()
            ->where('voucher_id', $voucherId)
            ->delete();

        VoucherApproval::withoutGlobalScopes()
            ->where('voucher_id', $voucherId)
            ->delete();

        TransactionAttachment::withoutGlobalScopes()
            ->where('company_id', $voucher->company_id)
            ->where('attachable_type', $voucher->getMorphClass())
            ->where('attachable_id', $voucherId)
            ->get()
            ->each(fn (TransactionAttachment $attachment) => app(VoucherAttachmentService::class)->delete($attachment));

        VoucherLine::withoutGlobalScopes()
            ->where('voucher_id', $voucherId)
            ->delete();

        $voucher->forceDelete();

        AuditLog::record('delete', 'vouchers', (string) $voucherId,
            null, null, "Voucher permanently deleted: {$number}");
    }
}
