<?php

namespace App\Domain\Accounting\Services;

use App\Domain\Accounting\Enums\PeriodStatus;
use App\Domain\Accounting\Models\FinancialPeriod;
use App\Domain\Accounting\Models\FinancialYear;
use App\Domain\Identity\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * The posting cutoff (§46).
 *
 * A company that has not defined its periods yet can post freely — that is how
 * an agency starts. Once periods exist, a closed one refuses new entries so
 * last month's reported figures stay reported.
 */
class PeriodGuard
{
    /** @throws ValidationException */
    public function assertOpen(int $companyId, string $date): void
    {
        $period = FinancialPeriod::covering($companyId, $date)->first();

        if ($period === null || $period->status->acceptsPostings()) {
            return;
        }

        throw ValidationException::withMessages([
            'entry_date' => [
                "{$period->name} is {$period->status->label()}; nothing further can be posted into it.",
            ],
        ]);
    }

    /**
     * Creates a year and its twelve monthly periods.
     *
     * @throws ValidationException
     */
    public function openYear(int $companyId, string $name, Carbon $startsOn, User $actor): FinancialYear
    {
        if (FinancialYear::where('company_id', $companyId)->where('name', $name)->exists()) {
            throw ValidationException::withMessages(['name' => ['That financial year already exists.']]);
        }

        return DB::transaction(function () use ($companyId, $name, $startsOn) {
            $endsOn = $startsOn->copy()->addYear()->subDay();

            $year = FinancialYear::create([
                'company_id' => $companyId,
                'name' => $name,
                'starts_on' => $startsOn->toDateString(),
                'ends_on' => $endsOn->toDateString(),
                'status' => PeriodStatus::Open->value,
            ]);

            $cursor = $startsOn->copy();

            for ($month = 0; $month < 12; $month++) {
                $periodStart = $cursor->copy();
                $periodEnd = $cursor->copy()->endOfMonth();

                FinancialPeriod::create([
                    'company_id' => $companyId,
                    'financial_year_id' => $year->getKey(),
                    'name' => $periodStart->format('M Y'),
                    'starts_on' => $periodStart->toDateString(),
                    'ends_on' => $periodEnd->toDateString(),
                    'status' => PeriodStatus::Open->value,
                ]);

                $cursor = $periodEnd->copy()->addDay()->startOfDay();
            }

            return $year->fresh('periods');
        });
    }

    /** @throws ValidationException */
    public function close(FinancialPeriod $period, User $actor): FinancialPeriod
    {
        if ($period->status !== PeriodStatus::Open) {
            throw ValidationException::withMessages([
                'period' => ["{$period->name} is already {$period->status->label()}."],
            ]);
        }

        $period->status = PeriodStatus::Closed;
        $period->closed_by = $actor->getKey();
        $period->closed_at = now();
        $period->save();

        return $period;
    }

    /**
     * Reopening is the exception, so it needs a reason and the permission
     * (§46: only authorised finance admins).
     *
     * @throws ValidationException
     */
    public function reopen(FinancialPeriod $period, string $reason, User $actor): FinancialPeriod
    {
        if (! $period->status->canReopen()) {
            throw ValidationException::withMessages([
                'period' => ["{$period->name} is {$period->status->label()} and cannot be reopened."],
            ]);
        }

        if (blank($reason)) {
            throw ValidationException::withMessages(['reason' => ['Say why the period is being reopened.']]);
        }

        $period->status = PeriodStatus::Open;
        $period->reopened_by = $actor->getKey();
        $period->reopened_at = now();
        $period->reopen_reason = $reason;
        $period->save();

        return $period;
    }

    /** @throws ValidationException */
    public function lock(FinancialPeriod $period, User $actor): FinancialPeriod
    {
        if ($period->status === PeriodStatus::Locked) {
            throw ValidationException::withMessages([
                'period' => ["{$period->name} is already locked."],
            ]);
        }

        $period->status = PeriodStatus::Locked;
        $period->closed_by = $actor->getKey();
        $period->closed_at ??= now();
        $period->save();

        return $period;
    }
}
