<?php

namespace App\Domain\Commissions\Services;

use App\Domain\Accounting\Services\BusinessEventPoster;
use App\Domain\Agents\Models\Agent;
use App\Domain\Audit\Enums\ActivityCategory;
use App\Domain\Audit\Services\ActivityLogger;
use App\Domain\Commissions\Enums\CommissionStatus;
use App\Domain\Commissions\Models\Commission;
use App\Domain\Identity\Models\User;
use App\Support\Money\Money;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Approving, clawing back and reporting on commission (§13A).
 */
class CommissionService
{
    public function __construct(
        private readonly SequenceGenerator $sequences,
        private readonly ActivityLogger $activity,
        private readonly BusinessEventPoster $books,
    ) {}

    /** @throws ValidationException */
    public function approve(Commission $commission, User $actor): Commission
    {
        if (! in_array($commission->status, [CommissionStatus::Accrued, CommissionStatus::UnderReview], true)) {
            throw ValidationException::withMessages([
                'commission' => ["A {$commission->status->label()} commission cannot be approved."],
            ]);
        }

        return DB::transaction(function () use ($commission, $actor) {
            $commission->status = CommissionStatus::Payable;
            $commission->approved_by = $actor->getKey();
            $commission->approved_at = now();
            $commission->payable_at = now();
            $commission->save();

            // Approval is what makes the commission owed (§35).
            $this->books->commissionApproved($commission, $actor);

            $this->activity->log(ActivityCategory::Payment, 'commission.approved', [
                'subject' => $commission->agent,
                'actor' => $actor,
                'title' => 'Commission approved',
                'description' => $commission->money()->format()." · {$commission->commission_no}",
                'visibility' => 'internal',
            ]);

            return $commission;
        });
    }

    /** @throws ValidationException */
    public function hold(Commission $commission, string $reason, User $actor): Commission
    {
        if (! $commission->status->isReversible() || $commission->status === CommissionStatus::Paid) {
            throw ValidationException::withMessages([
                'commission' => ["A {$commission->status->label()} commission cannot be held."],
            ]);
        }

        return DB::transaction(function () use ($commission, $reason, $actor) {
            $commission->status = CommissionStatus::UnderReview;
            $commission->notes = $reason;
            $commission->save();

            $this->activity->log(ActivityCategory::Payment, 'commission.held', [
                'subject' => $commission->agent,
                'actor' => $actor,
                'title' => 'Commission held for review',
                'description' => $reason,
                'visibility' => 'internal',
            ]);

            return $commission;
        });
    }

    /**
     * Clawback: a second, linked record. The original stays as it was (§13A).
     *
     * @throws ValidationException
     */
    public function reverse(Commission $commission, string $reason, User $actor): Commission
    {
        if (! $commission->status->isReversible()) {
            throw ValidationException::withMessages([
                'commission' => ["A {$commission->status->label()} commission cannot be reversed."],
            ]);
        }

        if ($commission->isReversal()) {
            throw ValidationException::withMessages([
                'commission' => ['A reversal cannot itself be reversed.'],
            ]);
        }

        if (blank($reason)) {
            throw ValidationException::withMessages(['reason' => ['A clawback must say why.']]);
        }

        return DB::transaction(function () use ($commission, $reason, $actor) {
            $reversal = Commission::create([
                'commission_no' => $this->nextNumber($commission->company_id),
                'company_id' => $commission->company_id,
                'branch_id' => $commission->branch_id,
                'agent_id' => $commission->agent_id,
                'applicant_id' => $commission->applicant_id,
                'job_demand_id' => $commission->job_demand_id,
                'employer_id' => $commission->employer_id,
                // Deliberately no rule: a clawback is not an earning event, and
                // leaving it null keeps the duplicate guard free for re-accrual.
                'commission_rule_id' => null,
                'trigger_event' => $commission->trigger_event->value,
                'calculation_basis' => $commission->calculation_basis->value,
                'eligible_amount' => $commission->eligible_amount,
                'rate_basis_points' => $commission->rate_basis_points,
                'commission_amount' => $commission->commission_amount,
                'currency' => $commission->currency,
                'status' => CommissionStatus::Reversed->value,
                'accrued_at' => now(),
                'reverses_commission_id' => $commission->getKey(),
                'reversal_reason' => $reason,
                'reversed_at' => now(),
                'reversed_by' => $actor->getKey(),
                'notes' => "Clawback of {$commission->commission_no}",
            ]);

            $commission->status = CommissionStatus::Reversed;
            $commission->reversed_at = now();
            $commission->reversed_by = $actor->getKey();
            $commission->reversal_reason = $reason;
            $commission->save();

            $this->activity->log(ActivityCategory::Payment, 'commission.reversed', [
                'subject' => $commission->agent,
                'actor' => $actor,
                'title' => 'Commission clawed back',
                'description' => $commission->money()->format()." · {$commission->commission_no}",
                'metadata' => ['reason' => $reason],
                'visibility' => 'internal',
            ]);

            return $reversal;
        });
    }

    /**
     * The agent's wallet (§13A ledger): what has been earned, taken back and
     * settled, and what is therefore still owed.
     *
     * @return array<string, mixed>
     */
    public function ledger(Agent $agent, string $currency): array
    {
        $base = fn () => Commission::where('agent_id', $agent->getKey())->where('currency', $currency);

        $accrued = (int) (clone $base())
            ->whereNull('reverses_commission_id')
            ->whereNotIn('status', [CommissionStatus::Cancelled->value])
            ->sum('commission_amount');

        $reversed = (int) (clone $base())
            ->whereNotNull('reverses_commission_id')
            ->sum('commission_amount');

        $paid = (int) (clone $base())->sum('paid_amount');

        $outstanding = (int) (clone $base())->outstanding()->sum(DB::raw('commission_amount - paid_amount'));

        return [
            'currency' => $currency,
            'accrued_amount' => $accrued,
            'reversed_amount' => $reversed,
            'paid_amount' => $paid,
            'outstanding_amount' => max(0, $outstanding),
            'awaiting_approval' => (int) (clone $base())->awaitingApproval()->sum('commission_amount'),
            'formatted_outstanding' => Money::of(max(0, $outstanding), $currency)->format(),
        ];
    }

    private function nextNumber(int $companyId): string
    {
        return $this->sequences->nextUnique(
            'commission',
            'COM',
            [now()->year],
            6,
            fn (string $no) => Commission::where('company_id', $companyId)
                ->where('commission_no', $no)
                ->exists(),
        );
    }
}
