<?php

namespace Modules\MedAccounting\Services;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Modules\MedAccounting\Entities\MedaccAccount;
use Modules\MedAccounting\Entities\MedaccJournalEntry;
use Modules\MedAccounting\Entities\MedaccJournalLine;
use Modules\MedAccounting\Entities\MedaccPeriod;

/**
 * LedgerService (SIFA P7-T2) — the balanced-posting engine at the heart of the GL.
 *
 * postEntry() validates that (a) the lines balance (Σdebit == Σcredit, both > 0),
 * (b) every account belongs to the business, and (c) the entry date is not inside
 * a CLOSED period, then writes a posted entry + its lines atomically.
 *
 * reverse() posts a mirror entry (debits↔credits swapped) referencing the
 * original — the accounting-correct way to undo a posted entry (posted entries
 * are never mutated in place, satisfying the spec's audit-trail requirement).
 *
 * balance()/accountBalance() are the shared aggregation helpers the reports use.
 *
 * This layer sits ON TOP of the ERP's payment-level double-entry — it reads the
 * ERP `transactions`/`transaction_payments` (via AutoPostingService) but posts
 * only into the medacc_* GL tables; the core app/Account + app/AccountTransaction
 * cash/bank ledger is left byte-identical.
 */
class LedgerService
{
    /**
     * Post a balanced journal entry (status = posted) with its lines.
     *
     * @param  array  $header  business_id, date, description, reference_type,
     *                         reference_id, created_by, status ('posted'|'draft')
     * @param  array  $lines   each: account_id, debit, credit, description?,
     *                         cost_center_id?
     *
     * @throws ValidationException when unbalanced / empty / period-locked / bad account
     */
    public function postEntry(array $header, array $lines): MedaccJournalEntry
    {
        $businessId = (int) ($header['business_id'] ?? 0);
        if ($businessId <= 0) {
            throw ValidationException::withMessages(['business_id' => __('medaccounting::lang.business_required')]);
        }

        $date = ! empty($header['date']) ? Carbon::parse($header['date'])->toDateString() : Carbon::now()->toDateString();
        $status = ($header['status'] ?? 'posted') === 'draft' ? 'draft' : 'posted';

        [$normLines, $totalDebit, $totalCredit] = $this->normalizeLines($businessId, $lines);

        if (empty($normLines)) {
            throw ValidationException::withMessages(['lines' => __('medaccounting::lang.no_lines')]);
        }

        // Balanced check (4-dp tolerance).
        if (round($totalDebit - $totalCredit, 4) !== 0.0) {
            throw ValidationException::withMessages([
                'lines' => __('medaccounting::lang.unbalanced', [
                    'debit'  => number_format($totalDebit, 2),
                    'credit' => number_format($totalCredit, 2),
                ]),
            ]);
        }

        if ($totalDebit <= 0.0) {
            throw ValidationException::withMessages(['lines' => __('medaccounting::lang.zero_entry')]);
        }

        // Period lock (only enforced for POSTED entries — drafts may be staged).
        if ($status === 'posted' && MedaccPeriod::isLocked($businessId, $date)) {
            throw ValidationException::withMessages([
                'date' => __('medaccounting::lang.period_locked', ['date' => $date]),
            ]);
        }

        return DB::transaction(function () use ($businessId, $header, $date, $status, $normLines, $totalDebit, $totalCredit) {
            $entry = MedaccJournalEntry::create([
                'business_id'    => $businessId,
                'entry_no'       => MedaccJournalEntry::nextEntryNo($businessId),
                'date'           => $date,
                'description'    => $header['description'] ?? null,
                'reference_type' => $header['reference_type'] ?? 'manual',
                'reference_id'   => $header['reference_id'] ?? null,
                'status'         => $status,
                'posted_by'      => $status === 'posted' ? ($header['created_by'] ?? null) : null,
                'posted_at'      => $status === 'posted' ? Carbon::now() : null,
                'reversal_of_id' => $header['reversal_of_id'] ?? null,
                'total_debit'    => $totalDebit,
                'total_credit'   => $totalCredit,
                'created_by'     => $header['created_by'] ?? null,
            ]);

            foreach ($normLines as $line) {
                MedaccJournalLine::create([
                    'business_id'      => $businessId,
                    'journal_entry_id' => $entry->id,
                    'account_id'       => $line['account_id'],
                    'debit'            => $line['debit'],
                    'credit'           => $line['credit'],
                    'description'      => $line['description'] ?? null,
                    'cost_center_id'   => $line['cost_center_id'] ?? null,
                ]);
            }

            return $entry->fresh('lines');
        });
    }

    /**
     * Post a mirror reversing entry for a posted entry and stamp the original
     * reversed. Idempotent: a second call is a no-op (returns the existing
     * reversal).
     */
    public function reverse(MedaccJournalEntry $entry, ?int $userId = null): MedaccJournalEntry
    {
        if ($entry->status !== 'posted') {
            throw ValidationException::withMessages([
                'status' => __('medaccounting::lang.only_posted_reversible'),
            ]);
        }

        $existing = MedaccJournalEntry::where('business_id', $entry->business_id)
            ->where('reversal_of_id', $entry->id)
            ->first();
        if ($existing) {
            return $existing;
        }

        $lines = $entry->lines->map(fn ($l) => [
            'account_id'     => $l->account_id,
            'debit'          => (float) $l->credit, // swap
            'credit'         => (float) $l->debit,  // swap
            'description'    => __('medaccounting::lang.reversal_of', ['no' => $entry->entry_no]),
            'cost_center_id' => $l->cost_center_id,
        ])->all();

        $reversal = $this->postEntry([
            'business_id'    => $entry->business_id,
            'date'           => Carbon::now()->toDateString(),
            'description'    => __('medaccounting::lang.reversal_of', ['no' => $entry->entry_no]),
            'reference_type' => $entry->reference_type,
            'reference_id'   => $entry->reference_id,
            'reversal_of_id' => $entry->id,
            'created_by'     => $userId,
        ], $lines);

        $entry->update([
            'status'      => 'reversed',
            'reversed_by' => $userId,
            'reversed_at' => Carbon::now(),
        ]);

        return $reversal;
    }

    /**
     * Aggregate posted debit/credit per account for a business, optionally within
     * a date range and/or cost centre. Returns keyed by account_id:
     *   [account_id => ['debit' => float, 'credit' => float]]
     */
    public function balance(int $businessId, ?string $from = null, ?string $to = null, ?int $costCenterId = null): array
    {
        $q = MedaccJournalLine::query()
            ->join('medacc_journal_entries as je', 'medacc_journal_lines.journal_entry_id', '=', 'je.id')
            ->where('medacc_journal_lines.business_id', $businessId)
            ->where('je.status', 'posted');

        if ($from) {
            $q->whereDate('je.date', '>=', $from);
        }
        if ($to) {
            $q->whereDate('je.date', '<=', $to);
        }
        if ($costCenterId) {
            $q->where('medacc_journal_lines.cost_center_id', $costCenterId);
        }

        $rows = $q->select(
            'medacc_journal_lines.account_id',
            DB::raw('SUM(medacc_journal_lines.debit) as debit'),
            DB::raw('SUM(medacc_journal_lines.credit) as credit')
        )->groupBy('medacc_journal_lines.account_id')->get();

        $out = [];
        foreach ($rows as $r) {
            $out[(int) $r->account_id] = [
                'debit'  => round((float) $r->debit, 4),
                'credit' => round((float) $r->credit, 4),
            ];
        }

        return $out;
    }

    /**
     * Signed balance of a single account (natural-side positive), optionally as of
     * a date and/or within a range.
     */
    public function accountBalance(MedaccAccount $account, ?string $from = null, ?string $to = null): float
    {
        $totals = $this->balance((int) $account->business_id, $from, $to)[$account->id] ?? ['debit' => 0.0, 'credit' => 0.0];

        return $account->signedBalance($totals['debit'], $totals['credit']);
    }

    // ── internals ──────────────────────────────────────────────────────────

    /**
     * Validate + normalise raw line input. Drops all-zero lines, clamps negatives,
     * verifies each account belongs to the business, and rolls up the totals.
     *
     * @return array{0:array,1:float,2:float} [lines, totalDebit, totalCredit]
     */
    protected function normalizeLines(int $businessId, array $lines): array
    {
        $accountIds = [];
        foreach ($lines as $l) {
            if (! empty($l['account_id'])) {
                $accountIds[] = (int) $l['account_id'];
            }
        }
        $validIds = MedaccAccount::where('business_id', $businessId)
            ->whereIn('id', array_unique($accountIds))
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $norm = [];
        $totalDebit = 0.0;
        $totalCredit = 0.0;

        foreach ($lines as $l) {
            $accountId = (int) ($l['account_id'] ?? 0);
            if (! in_array($accountId, $validIds, true)) {
                throw ValidationException::withMessages([
                    'lines' => __('medaccounting::lang.bad_account', ['id' => $accountId]),
                ]);
            }

            $debit = round(max(0.0, (float) ($l['debit'] ?? 0)), 4);
            $credit = round(max(0.0, (float) ($l['credit'] ?? 0)), 4);

            if ($debit === 0.0 && $credit === 0.0) {
                continue; // skip empty line
            }

            $norm[] = [
                'account_id'     => $accountId,
                'debit'          => $debit,
                'credit'         => $credit,
                'description'    => $l['description'] ?? null,
                'cost_center_id' => ! empty($l['cost_center_id']) ? (int) $l['cost_center_id'] : null,
            ];
            $totalDebit += $debit;
            $totalCredit += $credit;
        }

        return [$norm, round($totalDebit, 4), round($totalCredit, 4)];
    }
}
