<?php

namespace Modules\Accounting\Services;

use App\Models\ActivityLogModel;
use App\Models\AssetCategoryModel;
use App\Models\DepreciationRunLineModel;
use App\Models\DepreciationRunModel;
use App\Models\FixedAssetModel;
use DateTimeImmutable;

class DepreciationService
{
    protected DepreciationRunModel $runs;
    protected DepreciationRunLineModel $lines;
    protected FixedAssetModel $assets;
    protected AssetCategoryModel $categories;
    protected JournalEntryService $journals;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->runs       = new DepreciationRunModel();
        $this->lines      = new DepreciationRunLineModel();
        $this->assets     = new FixedAssetModel();
        $this->categories = new AssetCategoryModel();
        $this->journals   = new JournalEntryService();
        $this->activity   = new ActivityLogModel();
    }

    public function create(array $input, ?int $actingUserId): array
    {
        $periodStart = $input['period_start'] ?? '';
        $periodEnd   = $input['period_end'] ?? '';
        if ($periodStart === '' || $periodEnd === '') {
            return ['success' => false, 'errors' => ['period_start' => 'Period start and end are required.']];
        }
        if ($periodEnd < $periodStart) {
            return ['success' => false, 'errors' => ['period_end' => 'Period end must be on or after period start.']];
        }

        $computed = $this->computeLines($periodStart, $periodEnd);

        $data = [
            'number'       => $this->runs->nextNumber(),
            'period_start' => $periodStart,
            'period_end'   => $periodEnd,
            'run_date'     => $input['run_date'] ?? date('Y-m-d'),
            'total_amount' => $computed['total_amount'],
            'status'       => 'draft',
            'notes'        => ! empty($input['notes']) ? trim((string) $input['notes']) : null,
            'created_by'   => $actingUserId,
            'updated_by'   => $actingUserId,
        ];

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

        $id = (int) $this->runs->getInsertID();
        $this->lines->replaceForRun($id, $computed['lines']);
        $this->activity->log('depreciation_runs', 'created', $actingUserId, "Created depreciation run #{$id} ({$data['number']})", (string) $id);

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

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

        $periodStart = $input['period_start'] ?? $run['period_start'];
        $periodEnd   = $input['period_end'] ?? $run['period_end'];
        if ($periodEnd < $periodStart) {
            return ['success' => false, 'errors' => ['period_end' => 'Period end must be on or after period start.']];
        }

        $computed = $this->computeLines($periodStart, $periodEnd);
        $result = $this->lines->replaceForRun($id, $computed['lines']);

        $this->runs->update($id, [
            'period_start' => $periodStart,
            'period_end'   => $periodEnd,
            'run_date'     => $input['run_date'] ?? $run['run_date'],
            'total_amount' => $result['total_amount'],
            'notes'        => ! empty($input['notes']) ? trim((string) $input['notes']) : null,
            'updated_by'   => $actingUserId,
        ]);

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

        return ['success' => true];
    }

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

        $lines = $this->lines->getForRun($id);
        if (count($lines) === 0) {
            return ['success' => false, 'error' => 'Depreciation run has no lines to post.'];
        }

        $expenseBuckets = [];
        $accumBuckets   = [];
        $assetUpdates   = [];

        foreach ($lines as $line) {
            $asset = $this->assets->find((int) $line['fixed_asset_id']);
            if (! $asset || $asset['status'] !== 'active') {
                return ['success' => false, 'error' => 'One or more assets are not active and cannot be depreciated.'];
            }

            $category = $this->categories->find((int) $asset['category_id']);
            $expenseId = (int) ($asset['depr_expense_account_id'] ?: ($category['depr_expense_account_id'] ?? 0));
            $accumId   = (int) ($asset['accum_depr_account_id'] ?: ($category['accum_depr_account_id'] ?? 0));

            if ($expenseId <= 0 || $accumId <= 0) {
                return ['success' => false, 'error' => 'Asset "' . $asset['name'] . '" is missing depreciation expense or accumulated depreciation accounts.'];
            }

            $amount = round((float) $line['amount'], 2);
            $expenseBuckets[$expenseId] = ($expenseBuckets[$expenseId] ?? 0) + $amount;
            $accumBuckets[$accumId]     = ($accumBuckets[$accumId] ?? 0) + $amount;

            $newAccum = round((float) $asset['accum_depreciation'] + $amount, 2);
            $newBook  = round((float) $asset['book_value'] - $amount, 2);
            $salvage  = (float) $asset['salvage_value'];
            $status   = $newBook <= $salvage + 0.00001 ? 'fully_depreciated' : 'active';

            $assetUpdates[] = [
                'id'                 => (int) $asset['id'],
                'accum_depreciation' => $newAccum,
                'book_value'         => max(0, $newBook),
                'status'             => $status,
            ];
        }

        $jeLines = [];
        foreach ($expenseBuckets as $accountId => $amount) {
            $jeLines[] = [
                'account_id'  => $accountId,
                'description' => 'Depreciation ' . $run['number'],
                'debit'       => round($amount, 2),
                'credit'      => 0,
            ];
        }
        foreach ($accumBuckets as $accountId => $amount) {
            $jeLines[] = [
                'account_id'  => $accountId,
                'description' => 'Accumulated depreciation ' . $run['number'],
                'debit'       => 0,
                'credit'      => round($amount, 2),
            ];
        }

        $posted = $this->journals->createAndPost([
            'entry_date'  => $run['run_date'],
            'reference'   => $run['number'],
            'description' => 'Depreciation run ' . $run['number'] . ' (' . $run['period_start'] . ' to ' . $run['period_end'] . ')',
            'lines'       => $jeLines,
        ], $actingUserId);

        if (! $posted['success']) {
            return [
                'success' => false,
                'error'   => $posted['error'] ?? ($posted['errors']['lines'] ?? 'Failed to post depreciation journal entry.'),
            ];
        }

        foreach ($assetUpdates as $update) {
            $assetId = $update['id'];
            unset($update['id']);
            $update['updated_by'] = $actingUserId;
            $this->assets->update($assetId, $update);
        }

        $this->runs->update($id, [
            'status'           => 'posted',
            'journal_entry_id' => (int) $posted['id'],
            'posted_at'        => date('Y-m-d H:i:s'),
            'updated_by'       => $actingUserId,
        ]);
        $this->activity->log('depreciation_runs', 'posted', $actingUserId, "Posted depreciation run #{$id}", (string) $id);

        return ['success' => true];
    }

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

        $this->lines->where('run_id', $id)->delete();
        $this->runs->delete($id);
        $this->activity->log('depreciation_runs', 'deleted', $actingUserId, "Deleted depreciation run #{$id}", (string) $id);

        return ['success' => true];
    }

    /**
     * @return array{lines: list<array>, total_amount: float}
     */
    protected function computeLines(string $periodStart, string $periodEnd): array
    {
        $monthCount = $this->monthCount($periodStart, $periodEnd);
        $lines = [];
        $total = 0.0;

        foreach ($this->assets->getActiveDepreciable() as $asset) {
            $life = (int) ($asset['useful_life_months'] ?: $asset['category_useful_life_months'] ?: 0);
            if ($life <= 0) {
                continue;
            }

            $cost     = (float) $asset['cost'];
            $salvage  = (float) $asset['salvage_value'];
            $book     = (float) $asset['book_value'];
            $monthly  = ($cost - $salvage) / $life;
            $remaining = max(0, $book - $salvage);
            $amount   = round(min($monthly * $monthCount, $remaining), 2);

            if ($amount <= 0) {
                continue;
            }

            $bookAfter = round($book - $amount, 2);
            $lines[] = [
                'fixed_asset_id'   => (int) $asset['id'],
                'amount'           => $amount,
                'book_value_after' => max(0, $bookAfter),
            ];
            $total += $amount;
        }

        return ['lines' => $lines, 'total_amount' => round($total, 2)];
    }

    protected function monthCount(string $periodStart, string $periodEnd): int
    {
        $start = new DateTimeImmutable($periodStart);
        $end   = new DateTimeImmutable($periodEnd);

        if ($start->format('Y-m') === $end->format('Y-m')) {
            return 1;
        }

        // Inclusive calendar months in the period (Jan–Mar = 3).
        $months = ((int) $end->format('Y') - (int) $start->format('Y')) * 12
            + ((int) $end->format('n') - (int) $start->format('n'))
            + 1;

        return max(1, $months);
    }
}
