<?php

namespace App\Http\Controllers\Api\V1\Accounting;

use App\Domain\Accounting\Enums\AccountType;
use App\Domain\Accounting\Models\FinancialPeriod;
use App\Domain\Accounting\Models\FinancialYear;
use App\Domain\Accounting\Models\JournalEntry;
use App\Domain\Accounting\Models\LedgerAccount;
use App\Domain\Accounting\Services\LedgerPoster;
use App\Domain\Accounting\Services\LedgerReporter;
use App\Domain\Accounting\Services\PeriodGuard;
use App\Domain\Applicants\Models\Applicant;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;

/** Chart of accounts, journals and reports (§32–§37). */
class AccountingController extends Controller
{
    public function __construct(
        private readonly LedgerPoster $ledger,
        private readonly LedgerReporter $reports,
        private readonly PeriodGuard $periods,
    ) {}

    public function accounts(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.view'), 403);

        $accounts = LedgerAccount::where('company_id', $request->user()->company_id)
            ->when($request->boolean('postable_only'), fn ($q) => $q->postable())
            ->orderBy('code')
            ->get();

        return ApiResponse::success(
            $accounts->map(fn (LedgerAccount $account) => [
                'id' => $account->uuid,
                'code' => $account->code,
                'name' => $account->name,
                'type' => $account->type->value,
                'type_label' => $account->type->label(),
                'normal_balance' => $account->normal_balance,
                'is_group' => $account->is_group,
                'is_active' => $account->is_active,
                'is_cash_equivalent' => $account->is_cash_equivalent,
                'is_system' => $account->is_system,
                'currency' => $account->currency,
                'balance' => $account->is_group ? null : $account->balance(),
            ]),
            meta: [
                'types' => array_map(
                    fn (AccountType $type) => [
                        'value' => $type->value,
                        'label' => $type->label(),
                        'normal_balance' => $type->normalBalance(),
                    ],
                    AccountType::cases(),
                ),
            ],
        );
    }

    public function journals(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.view'), 403);

        $entries = JournalEntry::query()
            ->with('lines.account:id,code,name')
            ->where('company_id', $request->user()->company_id)
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
            ->when($request->filled('from') && $request->filled('to'), fn ($q) => $q->between(
                $request->string('from')->toString(),
                $request->string('to')->toString(),
            ))
            ->when($request->filled('search'), fn ($q) => $q->where(
                fn ($inner) => $inner
                    ->where('journal_no', 'like', '%'.$request->string('search').'%')
                    ->orWhere('description', 'like', '%'.$request->string('search').'%'),
            ))
            ->latest('entry_date')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $entries->through(fn (JournalEntry $entry) => $this->presentEntry($entry)),
        );
    }

    public function storeJournal(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.journal_create'), 403);

        $validated = $request->validate([
            'description' => ['required', 'string', 'max:500'],
            'entry_date' => ['nullable', 'date'],
            'currency' => ['nullable', 'string', 'size:3'],
            'lines' => ['required', 'array', 'min:2', 'max:50'],
            'lines.*.account_code' => ['required', 'string', 'max:20'],
            'lines.*.debit' => ['nullable', 'integer', 'min:0'],
            'lines.*.credit' => ['nullable', 'integer', 'min:0'],
            'lines.*.memo' => ['nullable', 'string', 'max:255'],
            'lines.*.cost_center' => ['nullable', 'string', 'max:60'],
        ]);

        $entry = $this->ledger->post(
            $request->user()->company_id,
            $validated['description'],
            array_map(fn (array $line) => [
                'account' => $line['account_code'],
                'debit' => $line['debit'] ?? 0,
                'credit' => $line['credit'] ?? 0,
                'memo' => $line['memo'] ?? null,
                'cost_center' => $line['cost_center'] ?? null,
            ], $validated['lines']),
            $request->user(),
            [
                'entry_date' => $validated['entry_date'] ?? null,
                'currency' => $validated['currency'] ?? config('mams.money.base_currency'),
            ],
        );

        return ApiResponse::success(
            $this->presentEntry($entry->fresh('lines.account')),
            "Posted as {$entry->journal_no}.",
            status: 201,
        );
    }

    public function reverseJournal(Request $request, JournalEntry $entry): JsonResponse
    {
        abort_unless($request->user()->can('accounting.journal_create'), 403);
        abort_unless($entry->company_id === $request->user()->company_id, 403);

        $validated = $request->validate(['reason' => ['required', 'string', 'max:1000']]);

        $reversal = $this->ledger->reverse($entry, $validated['reason'], $request->user());

        return ApiResponse::success(
            $this->presentEntry($reversal->fresh('lines.account')),
            'Reversed. The original entry is kept.',
        );
    }

    public function trialBalance(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view'), 403);

        [$from, $to] = $this->period($request);

        return ApiResponse::success(
            $this->reports->trialBalance($request->user()->company_id, $from, $to),
        );
    }

    public function incomeStatement(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view'), 403);

        [$from, $to] = $this->period($request);

        return ApiResponse::success(
            $this->reports->incomeStatement($request->user()->company_id, $from, $to),
        );
    }

    public function balanceSheet(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view'), 403);

        $validated = $request->validate(['as_at' => ['nullable', 'date']]);

        return ApiResponse::success($this->reports->balanceSheet(
            $request->user()->company_id,
            $validated['as_at'] ?? now()->toDateString(),
        ));
    }

    public function receivablesAging(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view') || $request->user()->can('billing.view'), 403);

        return ApiResponse::success($this->reports->receivablesAging($request->user()->company_id));
    }

    public function profitability(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view'), 403);

        [$from, $to] = $this->period($request);

        return ApiResponse::success(
            $this->reports->applicantProfitability($request->user()->company_id, $from, $to),
        );
    }

    public function revenueBy(Request $request, string $dimension): JsonResponse
    {
        abort_unless($request->user()->can('accounting.report_view'), 403);
        abort_unless(in_array($dimension, ['branch', 'agent', 'destination'], true), 404);

        [$from, $to] = $this->period($request);

        return ApiResponse::success(
            $this->reports->revenueBy($dimension, $request->user()->company_id, $from, $to),
        );
    }

    public function periods(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.view'), 403);

        $years = FinancialYear::with('periods')
            ->where('company_id', $request->user()->company_id)
            ->orderByDesc('starts_on')
            ->get();

        return ApiResponse::success(
            $years->map(fn (FinancialYear $year) => [
                'id' => $year->uuid,
                'name' => $year->name,
                'starts_on' => $year->starts_on->toDateString(),
                'ends_on' => $year->ends_on->toDateString(),
                'status' => $year->status->value,
                'periods' => $year->periods->map(fn (FinancialPeriod $period) => [
                    'id' => $period->uuid,
                    'name' => $period->name,
                    'starts_on' => $period->starts_on->toDateString(),
                    'ends_on' => $period->ends_on->toDateString(),
                    'status' => $period->status->value,
                    'status_label' => $period->status->label(),
                    'accepts_postings' => $period->status->acceptsPostings(),
                    'can_reopen' => $period->status->canReopen(),
                    'reopen_reason' => $period->reopen_reason,
                ]),
            ]),
        );
    }

    public function openYear(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.period_close'), 403);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:40'],
            'starts_on' => ['required', 'date'],
        ]);

        $year = $this->periods->openYear(
            $request->user()->company_id,
            $validated['name'],
            Carbon::parse($validated['starts_on']),
            $request->user(),
        );

        return ApiResponse::success(
            ['id' => $year->uuid, 'name' => $year->name, 'periods' => $year->periods->count()],
            "{$year->name} opened with 12 periods.",
            status: 201,
        );
    }

    public function closePeriod(Request $request, FinancialPeriod $period): JsonResponse
    {
        abort_unless($request->user()->can('accounting.period_close'), 403);
        abort_unless($period->company_id === $request->user()->company_id, 403);

        $closed = $this->periods->close($period, $request->user());

        return ApiResponse::success(
            ['id' => $closed->uuid, 'status' => $closed->status->value],
            "{$closed->name} closed. Nothing further can be posted into it.",
        );
    }

    public function reopenPeriod(Request $request, FinancialPeriod $period): JsonResponse
    {
        abort_unless($request->user()->can('accounting.period_close'), 403);
        abort_unless($period->company_id === $request->user()->company_id, 403);

        $validated = $request->validate(['reason' => ['required', 'string', 'max:1000']]);

        $reopened = $this->periods->reopen($period, $validated['reason'], $request->user());

        return ApiResponse::success(
            ['id' => $reopened->uuid, 'status' => $reopened->status->value],
            "{$reopened->name} reopened.",
        );
    }

    public function lockPeriod(Request $request, FinancialPeriod $period): JsonResponse
    {
        abort_unless($request->user()->can('accounting.period_close'), 403);
        abort_unless($period->company_id === $request->user()->company_id, 403);

        $locked = $this->periods->lock($period, $request->user());

        return ApiResponse::success(
            ['id' => $locked->uuid, 'status' => $locked->status->value],
            "{$locked->name} locked.",
        );
    }

    public function cashPosition(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('accounting.view'), 403);

        return ApiResponse::success($this->reports->cashPosition($request->user()->company_id));
    }

    public function applicantStatement(Request $request, Applicant $applicant): JsonResponse
    {
        $this->authorize('view', $applicant);
        abort_unless($request->user()->can('billing.view'), 403);

        return ApiResponse::success($this->reports->applicantStatement($applicant));
    }

    /** @return array{0: string, 1: string} */
    private function period(Request $request): array
    {
        $validated = $request->validate([
            'from' => ['nullable', 'date'],
            'to' => ['nullable', 'date', 'after_or_equal:from'],
        ]);

        return [
            $validated['from'] ?? now()->startOfYear()->toDateString(),
            $validated['to'] ?? now()->toDateString(),
        ];
    }

    /** @return array<string, mixed> */
    private function presentEntry(JournalEntry $entry): array
    {
        return [
            'id' => $entry->uuid,
            'journal_no' => $entry->journal_no,
            'entry_date' => $entry->entry_date?->toDateString(),
            'description' => $entry->description,
            'source' => $entry->source,
            'status' => $entry->status->value,
            'status_label' => $entry->status->label(),
            'is_reversal' => $entry->isReversal(),
            'currency' => $entry->currency,
            'total_debit' => (int) $entry->total_debit,
            'total_credit' => (int) $entry->total_credit,
            'is_balanced' => $entry->isBalanced(),
            'reversal_reason' => $entry->reversal_reason,
            'posted_at' => $entry->posted_at?->toIso8601String(),
            'lines' => $entry->relationLoaded('lines')
                ? $entry->lines->map(fn ($line) => [
                    'account_code' => $line->account?->code,
                    'account_name' => $line->account?->name,
                    'debit' => (int) $line->debit,
                    'credit' => (int) $line->credit,
                    'memo' => $line->memo,
                ])
                : [],
        ];
    }
}
