<?php

namespace Modules\Accounting\Services;

use App\Models\ActivityLogModel;
use App\Models\BankAccountModel;
use App\Models\CashAccountModel;
use App\Models\ChartOfAccountModel;
use App\Models\CostCenterModel;
use App\Models\ExpenseModel;

class ExpenseService
{
    protected ExpenseModel $expenses;
    protected ChartOfAccountModel $accounts;
    protected BankAccountModel $banks;
    protected CashAccountModel $cash;
    protected CostCenterModel $costCenters;
    protected JournalEntryService $journals;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->expenses    = new ExpenseModel();
        $this->accounts    = new ChartOfAccountModel();
        $this->banks       = new BankAccountModel();
        $this->cash        = new CashAccountModel();
        $this->costCenters = new CostCenterModel();
        $this->journals    = new JournalEntryService();
        $this->activity    = new ActivityLogModel();
    }

    public function create(array $input, ?int $actingUserId): array
    {
        $mapped = $this->mapInput($input);
        $check  = $this->validate($mapped);
        if (! $check['success']) {
            return $check;
        }

        $data = $mapped;
        $data['number']     = $this->expenses->nextNumber();
        $data['status']     = 'draft';
        $data['created_by'] = $actingUserId;
        $data['updated_by'] = $actingUserId;

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

        $id = (int) $this->expenses->getInsertID();
        $this->activity->log('expenses', 'created', $actingUserId, "Created expense #{$id} ({$data['number']})", (string) $id);

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

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

        $mapped = $this->mapInput($input);
        $check  = $this->validate($mapped);
        if (! $check['success']) {
            return $check;
        }

        $data = $mapped;
        $data['updated_by'] = $actingUserId;

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

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

        return ['success' => true];
    }

    public function approve(int $id, ?int $actingUserId): array
    {
        $expense = $this->expenses->find($id);
        if (! $expense) {
            return ['success' => false, 'error' => 'Expense not found.'];
        }
        if ($expense['status'] !== 'draft') {
            return ['success' => false, 'error' => 'Only draft expenses can be approved.'];
        }

        $check = $this->validate([
            'amount'             => (float) $expense['amount'],
            'expense_account_id' => (int) $expense['expense_account_id'],
            'pay_from_type'      => $expense['pay_from_type'],
            'pay_from_id'        => (int) $expense['pay_from_id'],
            'cost_center_id'     => $expense['cost_center_id'] !== null ? (int) $expense['cost_center_id'] : null,
        ]);
        if (! $check['success']) {
            $err = $check['errors']['amount']
                ?? $check['errors']['expense_account_id']
                ?? $check['errors']['pay_from_id']
                ?? $check['errors']['cost_center_id']
                ?? 'Invalid expense.';

            return ['success' => false, 'error' => $err];
        }

        $payCoa = $this->resolveAccountId($expense['pay_from_type'], (int) $expense['pay_from_id']);
        if (! $payCoa) {
            return ['success' => false, 'error' => 'Could not resolve chart of accounts for pay-from account.'];
        }

        $expenseCoa = (int) $expense['expense_account_id'];
        if ($expenseCoa === $payCoa) {
            return ['success' => false, 'error' => 'Expense account and pay-from must map to different ledger accounts.'];
        }

        $amount = round((float) $expense['amount'], 2);
        $desc   = 'Expense ' . $expense['number'] . ($expense['payee'] ? ' — ' . $expense['payee'] : '');

        $je = $this->journals->createAndPost([
            'entry_date'  => $expense['expense_date'],
            'reference'   => $expense['reference'] ?: $expense['number'],
            'description' => $desc,
            'lines'       => [
                [
                    'account_id'  => $expenseCoa,
                    'description' => 'Expense ' . $expense['number'],
                    'debit'       => $amount,
                    'credit'      => 0,
                ],
                [
                    'account_id'  => $payCoa,
                    'description' => 'Payment for ' . $expense['number'],
                    'debit'       => 0,
                    'credit'      => $amount,
                ],
            ],
        ], $actingUserId);

        if (! $je['success']) {
            $err = $je['error'] ?? null;
            if (! $err && ! empty($je['errors'])) {
                $err = $je['errors']['lines'] ?? (is_array($je['errors']) ? reset($je['errors']) : null);
            }

            return ['success' => false, 'error' => $err ?: 'Failed to post journal entry.'];
        }

        $this->expenses->update($id, [
            'status'           => 'approved',
            'journal_entry_id' => (int) $je['id'],
            'approved_at'      => date('Y-m-d H:i:s'),
            'updated_by'       => $actingUserId,
        ]);
        $this->activity->log('expenses', 'approved', $actingUserId, "Approved expense #{$id}", (string) $id);

        return ['success' => true];
    }

    public function delete(int $id, ?int $actingUserId): array
    {
        $expense = $this->expenses->find($id);
        if (! $expense) {
            return ['success' => false, 'error' => 'Expense not found.'];
        }
        if ($expense['status'] !== 'draft') {
            return ['success' => false, 'error' => 'Only draft expenses can be deleted.'];
        }

        $this->expenses->delete($id);
        $this->activity->log('expenses', 'deleted', $actingUserId, "Deleted expense #{$id}", (string) $id);

        return ['success' => true];
    }

    public function resolveAccountId(string $type, int $id): ?int
    {
        if ($type === 'bank') {
            $row = $this->banks->find($id);
        } elseif ($type === 'cash') {
            $row = $this->cash->find($id);
        } else {
            return null;
        }

        if (! $row || empty($row['account_id'])) {
            return null;
        }

        return (int) $row['account_id'];
    }

    protected function validate(array $data): array
    {
        $amount = round((float) ($data['amount'] ?? 0), 2);
        if ($amount <= 0) {
            return ['success' => false, 'errors' => ['amount' => 'Amount must be greater than zero.']];
        }

        $accountId = (int) ($data['expense_account_id'] ?? 0);
        $account   = $this->accounts->find($accountId);
        if (! $account) {
            return ['success' => false, 'errors' => ['expense_account_id' => 'Expense account not found.']];
        }
        if (($account['status'] ?? '') !== 'active') {
            return ['success' => false, 'errors' => ['expense_account_id' => 'Expense account must be active.']];
        }
        if (empty($account['is_postable'])) {
            return ['success' => false, 'errors' => ['expense_account_id' => 'Expense account must be postable.']];
        }

        $payType = $data['pay_from_type'] ?? '';
        $payId   = (int) ($data['pay_from_id'] ?? 0);
        $side    = $this->loadSide($payType, $payId);
        if (! $side) {
            return ['success' => false, 'errors' => ['pay_from_id' => 'Pay-from account not found.']];
        }
        if (($side['status'] ?? '') !== 'active') {
            return ['success' => false, 'errors' => ['pay_from_id' => 'Pay-from account must be active.']];
        }
        if ((int) ($side['account_id'] ?? 0) <= 0) {
            return ['success' => false, 'errors' => ['pay_from_id' => 'Pay-from account must be linked to the chart of accounts.']];
        }

        $ccId = $data['cost_center_id'] ?? null;
        if ($ccId !== null && (int) $ccId > 0) {
            $cc = $this->costCenters->find((int) $ccId);
            if (! $cc) {
                return ['success' => false, 'errors' => ['cost_center_id' => 'Cost center not found.']];
            }
            if (($cc['status'] ?? '') !== 'active') {
                return ['success' => false, 'errors' => ['cost_center_id' => 'Cost center must be active.']];
            }
        }

        return ['success' => true];
    }

    protected function loadSide(string $type, int $id): ?array
    {
        if ($type === 'bank') {
            return $this->banks->find($id);
        }
        if ($type === 'cash') {
            return $this->cash->find($id);
        }

        return null;
    }

    protected function mapInput(array $input): array
    {
        $cc = $input['cost_center_id'] ?? null;
        $cc = ($cc === '' || $cc === null) ? null : (int) $cc;
        if ($cc !== null && $cc <= 0) {
            $cc = null;
        }

        return [
            'expense_date'       => $input['expense_date'] ?? date('Y-m-d'),
            'amount'             => round((float) ($input['amount'] ?? 0), 2),
            'expense_account_id' => (int) ($input['expense_account_id'] ?? 0),
            'pay_from_type'      => $input['pay_from_type'] ?? 'bank',
            'pay_from_id'        => (int) ($input['pay_from_id'] ?? 0),
            'cost_center_id'     => $cc,
            'payee'              => trim((string) ($input['payee'] ?? '')) ?: null,
            'reference'          => trim((string) ($input['reference'] ?? '')) ?: null,
            'notes'              => ! empty($input['notes']) ? trim((string) $input['notes']) : null,
        ];
    }
}
