<?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\Enums\SettlementStatus;
use App\Domain\Commissions\Models\AgentSettlement;
use App\Domain\Commissions\Models\Commission;
use App\Domain\Identity\Models\User;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Paying agents what they have earned (§13A settlement).
 *
 * The gross always comes from the commissions actually attached, never from
 * the request, and a settled commission cannot be picked up by a second
 * settlement.
 */
class SettlementService
{
    public function __construct(
        private readonly SequenceGenerator $sequences,
        private readonly ActivityLogger $activity,
        private readonly BusinessEventPoster $books,
    ) {}

    /**
     * Drafts a settlement, sweeping in every approved commission in the period.
     *
     * @throws ValidationException
     */
    public function draft(
        Agent $agent,
        Carbon $periodStart,
        Carbon $periodEnd,
        string $currency,
        User $actor,
    ): AgentSettlement {
        if ($periodEnd->isBefore($periodStart)) {
            throw ValidationException::withMessages([
                'period_end' => ['The period ends before it starts.'],
            ]);
        }

        $commissions = Commission::where('agent_id', $agent->getKey())
            ->where('currency', $currency)
            ->settleable()
            ->whereBetween('accrued_at', [$periodStart->startOfDay(), $periodEnd->endOfDay()])
            ->get();

        if ($commissions->isEmpty()) {
            throw ValidationException::withMessages([
                'period_start' => ['No approved commission falls in that period.'],
            ]);
        }

        return DB::transaction(function () use ($agent, $periodStart, $periodEnd, $currency, $actor, $commissions) {
            $settlement = AgentSettlement::create([
                'settlement_no' => $this->nextNumber($agent->company_id),
                'company_id' => $agent->company_id,
                'branch_id' => $agent->branch_id,
                'agent_id' => $agent->getKey(),
                'status' => SettlementStatus::Draft->value,
                'currency' => $currency,
                'period_start' => $periodStart->toDateString(),
                'period_end' => $periodEnd->toDateString(),
                'created_by' => $actor->getKey(),
            ]);

            Commission::whereIn('id', $commissions->pluck('id'))
                ->update(['agent_settlement_id' => $settlement->getKey()]);

            $this->recompute($settlement);

            $this->activity->log(ActivityCategory::Payment, 'settlement.drafted', [
                'subject' => $agent,
                'actor' => $actor,
                'title' => 'Settlement drafted',
                'description' => $settlement->settlement_no.' · '.$commissions->count().' commissions',
                'visibility' => 'internal',
            ]);

            return $settlement->fresh();
        });
    }

    /**
     * Records the deductions and re-derives the net (§13A ledger).
     *
     * @param  array<string, mixed>  $attributes
     *
     * @throws ValidationException
     */
    public function setDeductions(AgentSettlement $settlement, array $attributes, User $actor): AgentSettlement
    {
        if (! $settlement->status->isEditable()) {
            throw ValidationException::withMessages([
                'settlement' => ["A {$settlement->status->label()} settlement cannot be edited."],
            ]);
        }

        return DB::transaction(function () use ($settlement, $attributes) {
            $settlement->bonus_amount = (int) ($attributes['bonus_amount'] ?? 0);
            $settlement->advance_deduction = (int) ($attributes['advance_deduction'] ?? 0);
            $settlement->withholding_tax = (int) ($attributes['withholding_tax'] ?? 0);
            $settlement->other_deduction = (int) ($attributes['other_deduction'] ?? 0);
            $settlement->deduction_note = $attributes['deduction_note'] ?? null;
            $settlement->save();

            return $this->recompute($settlement);
        });
    }

    /** @throws ValidationException */
    public function submit(AgentSettlement $settlement, User $actor): AgentSettlement
    {
        if ($settlement->status !== SettlementStatus::Draft) {
            throw ValidationException::withMessages([
                'settlement' => ["A {$settlement->status->label()} settlement cannot be submitted."],
            ]);
        }

        $settlement->status = SettlementStatus::Submitted;
        $settlement->save();

        return $settlement;
    }

    /** @throws ValidationException */
    public function approve(AgentSettlement $settlement, User $actor): AgentSettlement
    {
        if (! in_array($settlement->status, [SettlementStatus::Submitted, SettlementStatus::Reviewed], true)) {
            throw ValidationException::withMessages([
                'settlement' => ["A {$settlement->status->label()} settlement cannot be approved."],
            ]);
        }

        if ($settlement->created_by === $actor->getKey() && ! $actor->can('commission.pay')) {
            throw ValidationException::withMessages([
                'settlement' => ['A settlement should be approved by someone other than whoever prepared it.'],
            ]);
        }

        return DB::transaction(function () use ($settlement, $actor) {
            $settlement->status = SettlementStatus::Approved;
            $settlement->approved_by = $actor->getKey();
            $settlement->approved_at = now();
            $settlement->save();

            $this->activity->log(ActivityCategory::Payment, 'settlement.approved', [
                'subject' => $settlement->agent,
                'actor' => $actor,
                'title' => 'Settlement approved',
                'description' => $settlement->net()->format()." · {$settlement->settlement_no}",
                'visibility' => 'internal',
            ]);

            return $settlement;
        });
    }

    /**
     * @param  array<string, mixed>  $attributes
     *
     * @throws ValidationException
     */
    public function pay(AgentSettlement $settlement, array $attributes, User $actor): AgentSettlement
    {
        if ($settlement->status !== SettlementStatus::Approved) {
            throw ValidationException::withMessages([
                'settlement' => ['Only an approved settlement can be paid.'],
            ]);
        }

        if ($settlement->net_payable <= 0) {
            throw ValidationException::withMessages([
                'settlement' => ['The deductions leave nothing to pay.'],
            ]);
        }

        if (blank($attributes['payment_method'] ?? null)) {
            throw ValidationException::withMessages([
                'payment_method' => ['Record how the agent was paid.'],
            ]);
        }

        $reference = $attributes['payment_reference'] ?? null;

        if (filled($reference) && AgentSettlement::where('company_id', $settlement->company_id)
            ->where('payment_reference', $reference)
            ->whereKeyNot($settlement->getKey())
            ->exists()) {
            throw ValidationException::withMessages([
                'payment_reference' => ['This reference has already been recorded.'],
            ]);
        }

        return DB::transaction(function () use ($settlement, $attributes, $actor, $reference) {
            $settlement->status = SettlementStatus::Paid;
            $settlement->payment_method = $attributes['payment_method'];
            $settlement->payment_reference = $reference;
            $settlement->paid_at = now();
            $settlement->paid_by = $actor->getKey();
            $settlement->save();

            // Each commission is marked paid for its own amount, so the agent
            // statement still adds up line by line.
            foreach ($settlement->commissions()->get() as $commission) {
                $commission->paid_amount = $commission->commission_amount;
                $commission->status = CommissionStatus::Paid;
                $commission->save();
            }

            $this->books->settlementPaid($settlement, $actor);

            $this->activity->log(ActivityCategory::Payment, 'settlement.paid', [
                'subject' => $settlement->agent,
                'actor' => $actor,
                'title' => 'Settlement paid',
                'description' => $settlement->net()->format()." · {$settlement->settlement_no}",
                'visibility' => 'internal',
            ]);

            return $settlement;
        });
    }

    /** @throws ValidationException */
    public function cancel(AgentSettlement $settlement, string $reason, User $actor): AgentSettlement
    {
        if ($settlement->status->isClosed()) {
            throw ValidationException::withMessages([
                'settlement' => ["A {$settlement->status->label()} settlement cannot be cancelled."],
            ]);
        }

        return DB::transaction(function () use ($settlement, $reason, $actor) {
            // The commissions go back into the pool for a later settlement.
            $settlement->commissions()->update(['agent_settlement_id' => null]);

            $settlement->status = SettlementStatus::Cancelled;
            $settlement->notes = $reason;
            $settlement->save();

            $this->activity->log(ActivityCategory::Payment, 'settlement.cancelled', [
                'subject' => $settlement->agent,
                'actor' => $actor,
                'title' => 'Settlement cancelled',
                'description' => $reason,
                'visibility' => 'internal',
            ]);

            return $settlement;
        });
    }

    /** Gross and net are always derived, never accepted from a request. */
    private function recompute(AgentSettlement $settlement): AgentSettlement
    {
        $commissions = $settlement->commissions()->get();

        $gross = (int) $commissions->where('reverses_commission_id', null)->sum('commission_amount');
        $reversals = (int) $commissions->whereNotNull('reverses_commission_id')->sum('commission_amount');

        $settlement->gross_commission = $gross;
        $settlement->reversal_amount = $reversals;
        $settlement->net_payable = max(0,
            $gross
            + (int) $settlement->bonus_amount
            - $reversals
            - (int) $settlement->advance_deduction
            - (int) $settlement->withholding_tax
            - (int) $settlement->other_deduction
        );
        $settlement->save();

        return $settlement;
    }

    private function nextNumber(int $companyId): string
    {
        return $this->sequences->nextUnique(
            'agent_settlement',
            'AST',
            [now()->year],
            5,
            fn (string $no) => AgentSettlement::where('company_id', $companyId)
                ->where('settlement_no', $no)
                ->exists(),
        );
    }
}
