<?php

namespace App\Services;

use App\Models\Account;
use App\Models\AuditLog;
use App\Models\Budget;
use App\Models\BudgetLine;
use App\Models\Customer;
use App\Models\Dimension;
use App\Models\DimensionValue;
use App\Models\FiscalYear;
use App\Support\QuickBooksChartOfAccounts;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class BudgetService
{
    public function __construct(
        private readonly FiscalYearService $fiscalYears,
    ) {
    }

    public function list(int $companyId): Collection
    {
        return Budget::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->with([
                'fiscalYear:id,name,start_date,end_date',
                'customer:id,code,name',
                'dim1Value:id,name',
                'dim2Value:id,name',
                'dim3Value:id,name',
            ])
            ->orderByDesc('id')
            ->get();
    }

    public function fiscalYears(int $companyId): Collection
    {
        return FiscalYear::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->orderByDesc('start_date')
            ->get(['id', 'name', 'start_date', 'end_date', 'is_current']);
    }

    public function currentFiscalYear(int $companyId): ?FiscalYear
    {
        return FiscalYear::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_current', true)
            ->first()
            ?? FiscalYear::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->orderByDesc('start_date')
                ->first();
    }

    public function budgetAccounts(int $companyId): Collection
    {
        return Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->postable()
            ->whereIn('type', ['income', 'expense'])
            ->orderBy('code')
            ->orderBy('name')
            ->get(['id', 'code', 'name', 'type', 'sub_type', 'detail_type'])
            ->map(function (Account $account) {
                return [
                    'id' => $account->id,
                    'code' => $account->code,
                    'name' => $account->name,
                    'label' => trim(($account->code ? $account->code . ' ' : '') . $account->name),
                    'category' => QuickBooksChartOfAccounts::detailLabelForAccount($account) ?: ucfirst(str_replace('_', ' ', (string) $account->sub_type)),
                ];
            });
    }

    public function dimensions(int $companyId): Collection
    {
        return Dimension::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_active', true)
            ->orderBy('slot')
            ->with(['values' => fn ($query) => $query
                ->where('is_active', true)
                ->whereNull('deleted_at')
                ->orderBy('sort_order')
                ->orderBy('name')])
            ->get();
    }

    public function jobs(int $companyId): Collection
    {
        return Customer::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_job', true)
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->orderBy('name')
            ->get(['id', 'code', 'name', 'parent_id']);
    }

    public function formOptions(int $companyId): array
    {
        return [
            'fiscalYears' => $this->fiscalYears($companyId),
            'currentFiscalYear' => $this->currentFiscalYear($companyId),
            'accounts' => $this->budgetAccounts($companyId),
            'dimensions' => $this->dimensions($companyId),
            'jobs' => $this->jobs($companyId),
            'scopeTypes' => [
                Budget::TYPE_GENERAL => 'Company general budget',
                Budget::TYPE_DIMENSION => 'Dimension budget',
                Budget::TYPE_JOB => 'Customer: Job budget',
            ],
        ];
    }

    /** @return list<array{key: string, label: string}> */
    public function periodMonths(string $from, string $to): array
    {
        try {
            $start = Carbon::parse($from)->startOfMonth();
            $end = Carbon::parse($to)->endOfMonth();
        } catch (\Throwable) {
            return [];
        }

        if ($end->lt($start)) {
            return [];
        }

        $months = [];
        $cursor = $start->copy();

        while ($cursor->lte($end)) {
            $months[] = [
                'key' => $cursor->format('Y-m'),
                'label' => $cursor->format('M') . '-' . $cursor->format('y'),
            ];
            $cursor->addMonth();
        }

        return $months;
    }

    public function lineAmountsForBudget(Budget $budget): array
    {
        $legacyYear = (int) ($budget->start_date?->year ?? now()->year);

        return $budget->lines()
            ->get(['account_id', 'period_amounts', ...BudgetLine::MONTHS])
            ->mapWithKeys(function (BudgetLine $line) use ($legacyYear) {
                $periodAmounts = collect($line->period_amounts ?? [])
                    ->mapWithKeys(fn ($amount, $key) => [$key => (float) $amount])
                    ->all();

                if ($periodAmounts !== []) {
                    return [$line->account_id => $periodAmounts];
                }

                return [$line->account_id => BudgetLine::legacyMonthAmounts($line, $legacyYear)];
            })
            ->all();
    }

    public function store(int $companyId, array $data): Budget
    {
        $payload = $this->validatedPayload($companyId, $data);
        $periodKeys = array_column($this->periodMonths($payload['start_date'], $payload['end_date']), 'key');
        $lines = $this->parseLines($data['lines'] ?? [], $periodKeys);

        $budget = Budget::withoutGlobalScopes()->create([
            'company_id' => $companyId,
            ...$payload,
        ]);

        $this->syncLines($companyId, $budget, $lines, $payload);

        AuditLog::record('create', 'budgets', (string) $budget->id, null, [
            'name' => $budget->name,
            'type' => $budget->type,
        ], 'Budget created: ' . $budget->name);

        return $budget;
    }

    public function update(int $companyId, Budget $budget, array $data): Budget
    {
        $this->assertCompany($companyId, $budget);
        $payload = $this->validatedPayload($companyId, $data, $budget);
        $periodKeys = array_column($this->periodMonths($payload['start_date'], $payload['end_date']), 'key');
        $lines = $this->parseLines($data['lines'] ?? [], $periodKeys);

        $budget->update($payload);
        $budget->lines()->delete();
        $this->syncLines($companyId, $budget, $lines, $payload);

        AuditLog::record('update', 'budgets', (string) $budget->id, null, [
            'name' => $budget->name,
        ], 'Budget updated: ' . $budget->name);

        return $budget->fresh();
    }

    public function destroy(int $companyId, Budget $budget): void
    {
        $this->assertCompany($companyId, $budget);
        $name = $budget->name;
        $budget->delete();

        AuditLog::record('delete', 'budgets', (string) $budget->id, null, null, 'Budget deleted: ' . $name);
    }

    private function validatedPayload(int $companyId, array $data, ?Budget $existing = null): array
    {
        $type = $data['scope_type'] ?? Budget::TYPE_GENERAL;
        if (!in_array($type, [Budget::TYPE_GENERAL, Budget::TYPE_DIMENSION, Budget::TYPE_JOB], true)) {
            throw ValidationException::withMessages(['scope_type' => 'Invalid budget scope.']);
        }

        $periodFrom = trim((string) ($data['period_from'] ?? ''));
        $periodTo = trim((string) ($data['period_to'] ?? ''));

        if ($periodFrom === '' || $periodTo === '') {
            throw ValidationException::withMessages([
                'period_from' => 'Budget period from date is required.',
                'period_to' => 'Budget period to date is required.',
            ]);
        }

        try {
            $periodFrom = Carbon::parse($periodFrom)->toDateString();
            $periodTo = Carbon::parse($periodTo)->toDateString();
        } catch (\Throwable) {
            throw ValidationException::withMessages([
                'period_from' => 'Enter valid budget period dates.',
            ]);
        }

        if ($periodTo < $periodFrom) {
            throw ValidationException::withMessages([
                'period_to' => 'Budget period end date must be on or after the start date.',
            ]);
        }

        $fiscalYear = $this->fiscalYears->resolveForDate($companyId, $periodFrom, false);
        if (!$fiscalYear) {
            throw ValidationException::withMessages([
                'period_from' => 'No fiscal year found for the budget period start date.',
            ]);
        }

        $dimIds = ['dim1_id' => null, 'dim2_id' => null, 'dim3_id' => null, 'customer_id' => null];

        if ($type === Budget::TYPE_DIMENSION) {
            $dimensionValueId = (int) ($data['dimension_value_id'] ?? 0);
            $value = DimensionValue::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $dimensionValueId)
                ->where('is_active', true)
                ->first();

            if (!$value) {
                throw ValidationException::withMessages(['dimension_value_id' => 'Select a valid dimension value.']);
            }

            $slot = (int) Dimension::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $value->dimension_id)
                ->value('slot');

            if ($slot < 1 || $slot > 3) {
                throw ValidationException::withMessages(['dimension_value_id' => 'Only dimension slots 1–3 are supported for budgets.']);
            }

            $dimIds["dim{$slot}_id"] = $value->id;
        }

        if ($type === Budget::TYPE_JOB) {
            $jobId = (int) ($data['customer_id'] ?? 0);
            $job = Customer::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $jobId)
                ->where('is_job', true)
                ->where('is_active', true)
                ->first();

            if (!$job) {
                throw ValidationException::withMessages(['customer_id' => 'Select a valid customer job.']);
            }

            $dimIds['customer_id'] = $job->id;
        }

        return [
            'fiscal_year_id' => $fiscalYear->id,
            'start_date' => $periodFrom,
            'end_date' => $periodTo,
            'name' => trim((string) $data['name']),
            'type' => $type,
            'notes' => trim((string) ($data['notes'] ?? '')) ?: null,
            'is_active' => array_key_exists('is_active', $data) ? (bool) $data['is_active'] : true,
            ...$dimIds,
        ];
    }

    /** @return array<int, array<string, float>> */
    private function parseLines(array $lines, array $periodKeys): array
    {
        $parsed = [];

        foreach ($lines as $accountId => $months) {
            if (!is_array($months)) {
                continue;
            }

            $amounts = [];
            foreach ($periodKeys as $key) {
                $amounts[$key] = round((float) ($months[$key] ?? 0), 2);
            }

            if (BudgetLine::sumMonths($amounts) <= 0) {
                continue;
            }

            $parsed[(int) $accountId] = $amounts;
        }

        return $parsed;
    }

    private function syncLines(int $companyId, Budget $budget, array $lines, array $payload): void
    {
        $emptyMonths = array_fill_keys(BudgetLine::MONTHS, 0);

        foreach ($lines as $accountId => $amounts) {
            BudgetLine::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'budget_id' => $budget->id,
                'account_id' => $accountId,
                'dim1_id' => $payload['dim1_id'],
                'dim2_id' => $payload['dim2_id'],
                'dim3_id' => $payload['dim3_id'],
                'customer_id' => $payload['customer_id'],
                'period_amounts' => $amounts,
                ...$emptyMonths,
                'total' => BudgetLine::sumMonths($amounts),
            ]);
        }
    }

    private function assertCompany(int $companyId, Budget $budget): void
    {
        if ($budget->company_id !== $companyId) {
            abort(403);
        }
    }
}
