<?php
declare(strict_types=1);

namespace App\Modules\Accounting\Services;

use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Accounting\Models\FiscalYear;
use App\Modules\Accounting\Models\JournalEntry;
use App\Modules\Accounting\Models\PeriodClosing;

/**
 * Core journal entry service — all journal entries (manual + auto) pass through here.
 * Validates double-entry integrity, fiscal year boundaries, and period locks.
 */
final class JournalService
{
    /**
     * Create a journal entry with lines.
     *
     * @param array $header  [entry_date, description_ar, ?description_en, ?reference_type, ?reference_id,
     *                        ?reference_number, ?source_module, ?branch_id, ?cost_center_id, ?is_auto_generated, ?notes]
     * @param array $lines   Array of [account_id, debit, credit, ?description_ar, ?description_en,
     *                        ?cost_center_id, ?branch_id, ?member_id, ?employee_id, ?supplier_id, ?reference_type, ?reference_id]
     * @param bool  $autoPost Post immediately (true for auto-generated entries)
     *
     * @return array ['success' => bool, 'journal_entry_id' => int, 'entry_number' => string] or ['success' => false, 'error' => string]
     */
    public static function createEntry(array $header, array $lines, bool $autoPost = false): array
    {
        $db = App::getInstance()->db();

        // ── Validate ───────────────────────────────────────────
        if (empty($lines) || count($lines) < 2) {
            return ['success' => false, 'error' => 'قيد اليومية يجب أن يحتوي على سطرين على الأقل'];
        }

        $entryDate = $header['entry_date'] ?? date('Y-m-d');

        // Fiscal year check
        $fiscalYear = FiscalYear::findByDate($entryDate);
        if (!$fiscalYear) {
            return ['success' => false, 'error' => 'لا توجد سنة مالية مفتوحة لتاريخ ' . $entryDate];
        }
        if (!$fiscalYear->isOpen()) {
            return ['success' => false, 'error' => 'السنة المالية مغلقة — لا يمكن إضافة قيود'];
        }

        // Period closed check
        $period = substr($entryDate, 0, 7); // YYYY-MM
        if (PeriodClosing::isPeriodClosed((int) $fiscalYear->id, $period)) {
            return ['success' => false, 'error' => 'الفترة ' . $period . ' مغلقة — لا يمكن إضافة قيود'];
        }

        // Calculate totals and validate balance
        $totalDebit = '0.00';
        $totalCredit = '0.00';
        foreach ($lines as $line) {
            $debit = $line['debit'] ?? '0.00';
            $credit = $line['credit'] ?? '0.00';

            if (bccomp((string) $debit, '0.00', 2) > 0 && bccomp((string) $credit, '0.00', 2) > 0) {
                return ['success' => false, 'error' => 'لا يمكن أن يحتوي سطر واحد على مدين ودائن معاً'];
            }
            if (bccomp((string) $debit, '0.00', 2) === 0 && bccomp((string) $credit, '0.00', 2) === 0) {
                return ['success' => false, 'error' => 'كل سطر يجب أن يحتوي على قيمة مدينة أو دائنة'];
            }

            $totalDebit = bcadd($totalDebit, (string) $debit, 2);
            $totalCredit = bcadd($totalCredit, (string) $credit, 2);
        }

        if (bccomp($totalDebit, $totalCredit, 2) !== 0) {
            return ['success' => false, 'error' => 'مجموع المدين (' . $totalDebit . ') لا يساوي مجموع الدائن (' . $totalCredit . ')'];
        }

        if (bccomp($totalDebit, '0.00', 2) <= 0) {
            return ['success' => false, 'error' => 'مجموع القيد يجب أن يكون أكبر من صفر'];
        }

        // Validate accounts exist and are postable
        $accountIds = array_unique(array_column($lines, 'account_id'));
        $placeholders = implode(',', array_fill(0, count($accountIds), '?'));
        $accounts = $db->select(
            "SELECT id, is_header, is_active FROM chart_of_accounts WHERE id IN ({$placeholders})",
            array_values($accountIds)
        );

        $accountMap = [];
        foreach ($accounts as $acc) {
            $accountMap[(int) $acc['id']] = $acc;
        }

        foreach ($accountIds as $accId) {
            if (!isset($accountMap[(int) $accId])) {
                return ['success' => false, 'error' => 'الحساب رقم ' . $accId . ' غير موجود'];
            }
            if ((int) $accountMap[(int) $accId]['is_header'] === 1) {
                return ['success' => false, 'error' => 'لا يمكن الترحيل إلى حساب رئيسي (header)'];
            }
            if ((int) $accountMap[(int) $accId]['is_active'] === 0) {
                return ['success' => false, 'error' => 'الحساب غير نشط'];
            }
        }

        // ── Create ─────────────────────────────────────────────
        $db->beginTransaction();
        try {
            $entryNumber = JournalEntry::generateEntryNumber();

            $entryId = $db->insert('journal_entries', [
                'entry_number'      => $entryNumber,
                'fiscal_year_id'    => $fiscalYear->id,
                'entry_date'        => $entryDate,
                'reference_type'    => $header['reference_type'] ?? null,
                'reference_id'      => $header['reference_id'] ?? null,
                'reference_number'  => $header['reference_number'] ?? null,
                'description_ar'    => $header['description_ar'],
                'description_en'    => $header['description_en'] ?? null,
                'total_debit'       => $totalDebit,
                'total_credit'      => $totalCredit,
                'currency'          => $header['currency'] ?? 'EGP',
                'status'            => $autoPost ? 'posted' : 'draft',
                'posted_at'         => $autoPost ? date('Y-m-d H:i:s') : null,
                'posted_by'         => $autoPost ? self::currentEmployeeId() : null,
                'is_auto_generated' => (int) ($header['is_auto_generated'] ?? 0),
                'source_module'     => $header['source_module'] ?? null,
                'branch_id'         => $header['branch_id'] ?? null,
                'cost_center_id'    => $header['cost_center_id'] ?? null,
                'notes'             => $header['notes'] ?? null,
                'created_at'        => date('Y-m-d H:i:s'),
                'updated_at'        => date('Y-m-d H:i:s'),
                'created_by'        => self::currentEmployeeId(),
            ]);

            foreach ($lines as $i => $line) {
                $db->insert('journal_entry_lines', [
                    'journal_entry_id' => $entryId,
                    'line_number'      => $i + 1,
                    'account_id'       => (int) $line['account_id'],
                    'description_ar'   => $line['description_ar'] ?? null,
                    'description_en'   => $line['description_en'] ?? null,
                    'debit'            => $line['debit'] ?? '0.00',
                    'credit'           => $line['credit'] ?? '0.00',
                    'currency'         => $line['currency'] ?? 'EGP',
                    'cost_center_id'   => $line['cost_center_id'] ?? $header['cost_center_id'] ?? null,
                    'branch_id'        => $line['branch_id'] ?? $header['branch_id'] ?? null,
                    'member_id'        => $line['member_id'] ?? null,
                    'employee_id'      => $line['employee_id'] ?? null,
                    'supplier_id'      => $line['supplier_id'] ?? null,
                    'reference_type'   => $line['reference_type'] ?? null,
                    'reference_id'     => $line['reference_id'] ?? null,
                    'notes'            => $line['notes'] ?? null,
                    'created_at'       => date('Y-m-d H:i:s'),
                ]);
            }

            // If auto-post, update account balances immediately
            if ($autoPost) {
                self::updateAccountBalances($entryId, $lines, $entryDate, (int) $fiscalYear->id);
            }

            $db->commit();

            EventBus::dispatch('accounting.journal_entry.created', [
                'journal_entry_id' => $entryId,
                'entry_number'     => $entryNumber,
                'total_debit'      => $totalDebit,
                'source_module'    => $header['source_module'] ?? 'manual',
                'auto_posted'      => $autoPost,
            ]);

            Logger::info("Journal entry created", [
                'entry_id'     => $entryId,
                'entry_number' => $entryNumber,
                'total'        => $totalDebit,
                'auto'         => $autoPost,
            ]);

            return [
                'success'          => true,
                'journal_entry_id' => $entryId,
                'entry_number'     => $entryNumber,
            ];

        } catch (\Throwable $e) {
            $db->rollBack();
            Logger::error("Journal entry creation failed: " . $e->getMessage());
            return ['success' => false, 'error' => 'فشل إنشاء القيد: ' . $e->getMessage()];
        }
    }

    /**
     * Post a draft journal entry.
     */
    public static function postEntry(int $entryId): array
    {
        $db = App::getInstance()->db();

        $entry = JournalEntry::find($entryId);
        if (!$entry) {
            return ['success' => false, 'error' => 'القيد غير موجود'];
        }
        if ($entry->isPosted()) {
            return ['success' => false, 'error' => 'القيد مرحّل بالفعل'];
        }
        if ($entry->isReversed()) {
            return ['success' => false, 'error' => 'القيد ملغي — لا يمكن ترحيله'];
        }

        // Re-validate period is open
        $period = substr($entry->entry_date, 0, 7);
        if (PeriodClosing::isPeriodClosed((int) $entry->fiscal_year_id, $period)) {
            return ['success' => false, 'error' => 'الفترة مغلقة — لا يمكن ترحيل القيد'];
        }

        $db->beginTransaction();
        try {
            $db->update('journal_entries', [
                'status'     => 'posted',
                'posted_at'  => date('Y-m-d H:i:s'),
                'posted_by'  => self::currentEmployeeId(),
                'updated_at' => date('Y-m-d H:i:s'),
                'updated_by' => self::currentEmployeeId(),
            ], '`id` = ?', [$entryId]);

            $lines = $db->select(
                "SELECT * FROM journal_entry_lines WHERE journal_entry_id = ? ORDER BY line_number",
                [$entryId]
            );

            self::updateAccountBalances($entryId, $lines, $entry->entry_date, (int) $entry->fiscal_year_id);

            $db->commit();

            EventBus::dispatch('accounting.journal_entry.posted', [
                'journal_entry_id' => $entryId,
                'entry_number'     => $entry->entry_number,
            ]);

            return ['success' => true];
        } catch (\Throwable $e) {
            $db->rollBack();
            return ['success' => false, 'error' => 'فشل ترحيل القيد: ' . $e->getMessage()];
        }
    }

    /**
     * Reverse a posted journal entry by creating a mirror entry.
     */
    public static function reverseEntry(int $entryId, string $reason, ?string $reversalDate = null): array
    {
        $db = App::getInstance()->db();

        $entry = JournalEntry::find($entryId);
        if (!$entry) {
            return ['success' => false, 'error' => 'القيد غير موجود'];
        }
        if (!$entry->isPosted()) {
            return ['success' => false, 'error' => 'لا يمكن عكس قيد غير مرحّل'];
        }
        if ($entry->reversal_of_id !== null) {
            return ['success' => false, 'error' => 'القيد معكوس بالفعل'];
        }

        $revDate = $reversalDate ?? date('Y-m-d');
        $lines = $db->select(
            "SELECT * FROM journal_entry_lines WHERE journal_entry_id = ? ORDER BY line_number",
            [$entryId]
        );

        // Create reversed lines (swap debit/credit)
        $reversedLines = [];
        foreach ($lines as $line) {
            $reversedLines[] = [
                'account_id'     => $line['account_id'],
                'debit'          => $line['credit'],
                'credit'         => $line['debit'],
                'description_ar' => 'عكس: ' . ($line['description_ar'] ?? ''),
                'description_en' => $line['description_en'] ? 'Reversal: ' . $line['description_en'] : null,
                'cost_center_id' => $line['cost_center_id'],
                'branch_id'      => $line['branch_id'],
                'member_id'      => $line['member_id'],
                'employee_id'    => $line['employee_id'],
                'supplier_id'    => $line['supplier_id'],
            ];
        }

        $result = self::createEntry([
            'entry_date'        => $revDate,
            'description_ar'    => 'عكس قيد رقم ' . $entry->entry_number . ' — ' . $reason,
            'description_en'    => 'Reversal of ' . $entry->entry_number . ' — ' . $reason,
            'reference_type'    => 'reversal',
            'reference_id'      => $entryId,
            'reference_number'  => $entry->entry_number,
            'source_module'     => $entry->source_module,
            'branch_id'         => $entry->branch_id,
            'cost_center_id'    => $entry->cost_center_id,
            'is_auto_generated' => 1,
            'notes'             => $reason,
        ], $reversedLines, true);

        if (!$result['success']) {
            return $result;
        }

        // Mark original as reversed and link
        $db->update('journal_entries', [
            'status'          => 'reversed',
            'reversal_of_id'  => $result['journal_entry_id'],
            'updated_at'      => date('Y-m-d H:i:s'),
            'updated_by'      => self::currentEmployeeId(),
        ], '`id` = ?', [$entryId]);

        // Link reversal entry to original
        $db->update('journal_entries', [
            'reversed_entry_id' => $entryId,
        ], '`id` = ?', [$result['journal_entry_id']]);

        EventBus::dispatch('accounting.journal_entry.reversed', [
            'original_entry_id'  => $entryId,
            'reversal_entry_id'  => $result['journal_entry_id'],
            'reason'             => $reason,
        ]);

        return [
            'success'          => true,
            'reversal_entry_id' => $result['journal_entry_id'],
            'reversal_number'   => $result['entry_number'],
        ];
    }

    /**
     * Update account running balances when an entry is posted.
     */
    private static function updateAccountBalances(int $entryId, array $lines, string $entryDate, int $fiscalYearId): void
    {
        $db = App::getInstance()->db();
        $period = substr($entryDate, 0, 7);

        foreach ($lines as $line) {
            $accountId = (int) $line['account_id'];
            $debit = $line['debit'] ?? '0.00';
            $credit = $line['credit'] ?? '0.00';
            $costCenterId = $line['cost_center_id'] ?? null;
            $branchId = $line['branch_id'] ?? null;

            // Update chart_of_accounts.current_balance
            $account = $db->selectOne("SELECT account_nature, current_balance FROM chart_of_accounts WHERE id = ?", [$accountId]);
            if ($account) {
                $change = bcsub((string) $debit, (string) $credit, 2);
                if ($account['account_nature'] === 'credit') {
                    $change = bcsub((string) $credit, (string) $debit, 2);
                }
                $newBalance = bcadd((string) $account['current_balance'], $change, 2);
                $db->update('chart_of_accounts', ['current_balance' => $newBalance], '`id` = ?', [$accountId]);
            }

            // Upsert account_balances for the period
            $existing = $db->selectOne(
                "SELECT id, period_debit, period_credit FROM account_balances
                 WHERE account_id = ? AND fiscal_year_id = ? AND period = ?
                   AND (cost_center_id IS NULL AND ? IS NULL OR cost_center_id = ?)
                   AND (branch_id IS NULL AND ? IS NULL OR branch_id = ?)",
                [$accountId, $fiscalYearId, $period, $costCenterId, $costCenterId, $branchId, $branchId]
            );

            if ($existing) {
                $newDebit = bcadd((string) $existing['period_debit'], (string) $debit, 2);
                $newCredit = bcadd((string) $existing['period_credit'], (string) $credit, 2);
                $db->update('account_balances', [
                    'period_debit'  => $newDebit,
                    'period_credit' => $newCredit,
                    'closing_debit' => bcadd((string) ($existing['opening_debit'] ?? '0.00'), $newDebit, 2),
                    'closing_credit' => bcadd((string) ($existing['opening_credit'] ?? '0.00'), $newCredit, 2),
                    'updated_at'    => date('Y-m-d H:i:s'),
                ], '`id` = ?', [(int) $existing['id']]);
            } else {
                $db->insert('account_balances', [
                    'account_id'     => $accountId,
                    'fiscal_year_id' => $fiscalYearId,
                    'period'         => $period,
                    'opening_debit'  => '0.00',
                    'opening_credit' => '0.00',
                    'period_debit'   => $debit,
                    'period_credit'  => $credit,
                    'closing_debit'  => $debit,
                    'closing_credit' => $credit,
                    'cost_center_id' => $costCenterId,
                    'branch_id'      => $branchId,
                    'created_at'     => date('Y-m-d H:i:s'),
                    'updated_at'     => date('Y-m-d H:i:s'),
                ]);
            }
        }
    }

    private static function currentEmployeeId(): ?int
    {
        $emp = App::getInstance()->currentEmployee();
        if ($emp) {
            return (int) ($emp->id ?? $emp['id'] ?? null);
        }
        return null;
    }
}
