<?php

namespace Modules\Accounting\Services;

use App\Models\ActivityLogModel;
use App\Models\ChartOfAccountModel;
use App\Models\FiscalYearModel;
use App\Models\JournalEntryLineModel;
use App\Models\JournalEntryModel;

class JournalEntryService
{
    protected JournalEntryModel $journals;
    protected JournalEntryLineModel $lines;
    protected ChartOfAccountModel $accounts;
    protected FiscalYearModel $periods;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->journals = new JournalEntryModel();
        $this->lines    = new JournalEntryLineModel();
        $this->accounts = new ChartOfAccountModel();
        $this->periods  = new FiscalYearModel();
        $this->activity = new ActivityLogModel();
    }

    public function create(array $input, ?int $actingUserId): array
    {
        $lineCheck = $this->validateLines($input['lines'] ?? []);
        if (! $lineCheck['success']) {
            return $lineCheck;
        }

        $resolved = $this->resolveFiscalYear($input);
        if (! $resolved['success']) {
            return $resolved;
        }

        $data = $this->mapInput($input, $resolved['fiscal_year_id']);
        $data['number']     = $this->journals->nextNumber();
        $data['status']     = 'draft';
        $data['created_by'] = $actingUserId;
        $data['updated_by'] = $actingUserId;

        if (! $this->journals->insert($data, true)) {
            return ['success' => false, 'errors' => $this->journals->errors()];
        }

        $id = (int) $this->journals->getInsertID();
        $totals = $this->lines->replaceForJournalEntry($id, $input['lines'] ?? []);
        $this->journals->update($id, [
            'total_debit'  => $totals['total_debit'],
            'total_credit' => $totals['total_credit'],
        ]);

        $this->activity->log('journal_entries', 'created', $actingUserId, "Created journal entry #{$id} ({$data['number']})", (string) $id);

        return ['success' => true, 'id' => $id];
    }

    public function update(int $id, array $input, ?int $actingUserId): array
    {
        $journal = $this->journals->find($id);
        if (! $journal) {
            return ['success' => false, 'errors' => ['id' => 'Journal entry not found.']];
        }
        if ($journal['status'] !== 'draft') {
            return ['success' => false, 'errors' => ['status' => 'Only draft journal entries can be edited.']];
        }

        $lineCheck = $this->validateLines($input['lines'] ?? []);
        if (! $lineCheck['success']) {
            return $lineCheck;
        }

        $resolved = $this->resolveFiscalYear($input);
        if (! $resolved['success']) {
            return $resolved;
        }

        $data = $this->mapInput($input, $resolved['fiscal_year_id']);
        $data['updated_by'] = $actingUserId;

        if (! $this->journals->update($id, $data)) {
            return ['success' => false, 'errors' => $this->journals->errors()];
        }

        $totals = $this->lines->replaceForJournalEntry($id, $input['lines'] ?? []);
        $this->journals->update($id, [
            'total_debit'  => $totals['total_debit'],
            'total_credit' => $totals['total_credit'],
            'updated_by'   => $actingUserId,
        ]);

        $this->activity->log('journal_entries', 'updated', $actingUserId, "Updated journal entry #{$id}", (string) $id);

        return ['success' => true];
    }

    /**
     * Create a draft journal entry and immediately post it.
     *
     * @return array{success:bool,id?:int,errors?:array,error?:string}
     */
    public function createAndPost(array $input, ?int $actingUserId): array
    {
        $result = $this->create($input, $actingUserId);
        if (! $result['success']) {
            return $result;
        }

        $post = $this->post((int) $result['id'], $actingUserId);
        if (! $post['success']) {
            return $post;
        }

        return ['success' => true, 'id' => (int) $result['id']];
    }

    public function post(int $id, ?int $actingUserId): array
    {
        $journal = $this->journals->find($id);
        if (! $journal) {
            return ['success' => false, 'error' => 'Journal entry not found.'];
        }
        if ($journal['status'] !== 'draft') {
            return ['success' => false, 'error' => 'Only draft journal entries can be posted.'];
        }

        $lines = $this->lines->getForJournalEntry($id);
        $check = $this->validateLines($lines, true);
        if (! $check['success']) {
            return ['success' => false, 'error' => $check['errors']['lines'] ?? 'Journal entry is not balanced.'];
        }

        if (! empty($journal['fiscal_year_id'])) {
            $period = $this->periods->find((int) $journal['fiscal_year_id']);
            if (! $period || $period['status'] !== 'open') {
                return ['success' => false, 'error' => 'Cannot post into a closed financial period.'];
            }
        }

        $this->journals->update($id, [
            'status'     => 'posted',
            'posted_by'  => $actingUserId,
            'posted_at'  => date('Y-m-d H:i:s'),
            'updated_by' => $actingUserId,
        ]);
        $this->activity->log('journal_entries', 'posted', $actingUserId, "Posted journal entry #{$id}", (string) $id);

        return ['success' => true];
    }

    public function unpost(int $id, ?int $actingUserId): array
    {
        $journal = $this->journals->find($id);
        if (! $journal) {
            return ['success' => false, 'error' => 'Journal entry not found.'];
        }
        if ($journal['status'] !== 'posted') {
            return ['success' => false, 'error' => 'Only posted journal entries can be unposted.'];
        }

        if (! empty($journal['fiscal_year_id'])) {
            $period = $this->periods->find((int) $journal['fiscal_year_id']);
            if ($period && $period['status'] === 'closed') {
                return ['success' => false, 'error' => 'Cannot unpost entries in a closed financial period.'];
            }
        }

        $this->journals->update($id, [
            'status'     => 'draft',
            'posted_by'  => null,
            'posted_at'  => null,
            'updated_by' => $actingUserId,
        ]);
        $this->activity->log('journal_entries', 'unposted', $actingUserId, "Unposted journal entry #{$id}", (string) $id);

        return ['success' => true];
    }

    public function delete(int $id, ?int $actingUserId): array
    {
        $journal = $this->journals->find($id);
        if (! $journal) {
            return ['success' => false, 'error' => 'Journal entry not found.'];
        }
        if (! in_array($journal['status'], ['draft', 'cancelled'], true)) {
            return ['success' => false, 'error' => 'Only draft or cancelled journal entries can be deleted.'];
        }

        $this->lines->where('journal_entry_id', $id)->delete();
        $this->journals->delete($id);
        $this->activity->log('journal_entries', 'deleted', $actingUserId, "Deleted journal entry #{$id}", (string) $id);

        return ['success' => true];
    }

    protected function resolveFiscalYear(array $input): array
    {
        $fiscalYearId = (int) ($input['fiscal_year_id'] ?? 0);
        $entryDate = $input['entry_date'] ?? date('Y-m-d');

        if ($fiscalYearId > 0) {
            $period = $this->periods->find($fiscalYearId);
            if (! $period) {
                return ['success' => false, 'errors' => ['fiscal_year_id' => 'Financial period not found.']];
            }
            if ($period['status'] !== 'open') {
                return ['success' => false, 'errors' => ['fiscal_year_id' => 'Selected financial period is closed.']];
            }

            return ['success' => true, 'fiscal_year_id' => $fiscalYearId];
        }

        $period = $this->periods->findForDate($entryDate) ?? $this->periods->getCurrent();
        if ($period) {
            return ['success' => true, 'fiscal_year_id' => (int) $period['id']];
        }

        return ['success' => true, 'fiscal_year_id' => null];
    }

    protected function validateLines(array $lines, bool $asErrorString = false): array
    {
        $usable = [];
        $totalDebit = 0.0;
        $totalCredit = 0.0;

        foreach ($lines as $line) {
            $accountId = (int) ($line['account_id'] ?? 0);
            $debit  = round((float) ($line['debit'] ?? 0), 2);
            $credit = round((float) ($line['credit'] ?? 0), 2);

            if ($accountId <= 0 && $debit <= 0 && $credit <= 0) {
                continue;
            }

            if ($accountId <= 0) {
                $msg = 'Each journal line must have an account.';
                return $asErrorString
                    ? ['success' => false, 'errors' => ['lines' => $msg]]
                    : ['success' => false, 'errors' => ['lines' => $msg]];
            }

            $account = $this->accounts->find($accountId);
            if (! $account || $account['status'] !== 'active' || ! (int) $account['is_postable']) {
                $msg = 'One or more accounts are inactive or not postable.';
                return ['success' => false, 'errors' => ['lines' => $msg]];
            }

            if ($debit <= 0 && $credit <= 0) {
                $msg = 'Each journal line needs a debit or credit amount.';
                return ['success' => false, 'errors' => ['lines' => $msg]];
            }

            if ($debit > 0 && $credit > 0) {
                $msg = 'A journal line cannot have both debit and credit.';
                return ['success' => false, 'errors' => ['lines' => $msg]];
            }

            $totalDebit += $debit;
            $totalCredit += $credit;
            $usable[] = $line;
        }

        if (count($usable) < 2) {
            $msg = 'A journal entry needs at least two lines.';
            return ['success' => false, 'errors' => ['lines' => $msg]];
        }

        if (round($totalDebit, 2) !== round($totalCredit, 2) || $totalDebit <= 0) {
            $msg = 'Journal entry must balance: total debit must equal total credit.';
            return ['success' => false, 'errors' => ['lines' => $msg]];
        }

        return ['success' => true];
    }

    protected function mapInput(array $input, ?int $fiscalYearId): array
    {
        return [
            'entry_date'     => $input['entry_date'] ?? date('Y-m-d'),
            'fiscal_year_id' => $fiscalYearId,
            'reference'      => ! empty($input['reference']) ? trim((string) $input['reference']) : null,
            'description'    => ! empty($input['description']) ? trim((string) $input['description']) : null,
        ];
    }
}
