<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\AuditEvent;
use App\Models\Asset;
use App\Models\AssetWarranty;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

/**
 * The one thing plain Eloquent CRUD cannot do safely on its own: maintain
 * "at most one primary warranty per asset" across a create, an edit that
 * promotes a different row, and a renewal — three different call sites that
 * must all agree on how that flag moves.
 *
 * Document storage lives here too rather than in the controller, for the
 * same reason `AssetService` owns tag generation: it is business logic that
 * happens to also touch the filesystem, not filesystem code that happens to
 * live near a controller.
 */
class WarrantyService
{
    /**
     * The dashboard's three expiry buckets. Separate from the
     * `warranty_alert_days` setting, which paces the email digest — these
     * are fixed, coarse buckets for browsing and counting, not a reminder
     * schedule.
     *
     * @var list<int>
     */
    public const UI_BUCKETS = [30, 60, 90];

    public function __construct(
        private readonly AuditLogger $auditLogger,
    ) {}

    /**
     * @param  array<string, mixed>  $data  StoreWarrantyRequest::validated(), plus an optional 'document' UploadedFile and 'make_primary' bool
     */
    public function create(Asset $asset, array $data): AssetWarranty
    {
        return DB::transaction(function () use ($asset, $data): AssetWarranty {
            // An asset's very first warranty is always featured — there is
            // no meaningful sense in which an asset "has coverage" that
            // isn't shown as its current one.
            $makePrimary = (bool) ($data['make_primary'] ?? false)
                || ! AssetWarranty::query()->where('asset_id', $asset->id)->exists();
            unset($data['make_primary']);

            $document = $data['document'] ?? null;
            unset($data['document']);

            if ($makePrimary) {
                $this->clearPrimary($asset);
            }

            $warranty = new AssetWarranty;
            $warranty->fill($data);
            $warranty->asset_id = $asset->id;
            $warranty->is_primary = $makePrimary;
            $warranty->save();

            if ($document instanceof UploadedFile) {
                $this->attachDocument($warranty, $document);
            }

            return $warranty;
        });
    }

    /**
     * Field edits only — never touches `is_primary`. Promoting a different
     * warranty to primary happens through renew(), which is the one
     * operation that both closes one coverage period and opens another; an
     * edit-triggered promotion would let the same invariant be maintained in
     * two different places.
     *
     * @param  array<string, mixed>  $data  UpdateWarrantyRequest::validated(), plus an optional 'document' UploadedFile and 'remove_document' bool
     */
    public function update(AssetWarranty $warranty, array $data): AssetWarranty
    {
        $removeDocument = (bool) ($data['remove_document'] ?? false);
        unset($data['remove_document']);

        $document = $data['document'] ?? null;
        unset($data['document']);

        $warranty->update($data);

        if ($document instanceof UploadedFile) {
            $this->attachDocument($warranty, $document);
        } elseif ($removeDocument) {
            $this->removeDocument($warranty);
        }

        return $warranty;
    }

    /**
     * Close the current primary coverage period and open a new one in its
     * place, inside one transaction. The closed row is left exactly as it
     * was — only `is_primary` moves — which is what "renewal history" means
     * here: nothing about the old record changes except that it is no
     * longer the current one.
     *
     * @param  array<string, mixed>  $data  RenewWarrantyRequest::validated(), plus an optional 'document' UploadedFile
     */
    public function renew(AssetWarranty $current, array $data): AssetWarranty
    {
        return DB::transaction(function () use ($current, $data): AssetWarranty {
            /** @var AssetWarranty $locked */
            $locked = AssetWarranty::query()->whereKey($current->getKey())->with('asset')->lockForUpdate()->firstOrFail();

            $document = $data['document'] ?? null;
            unset($data['document']);

            $locked->is_primary = false;
            $locked->save();

            $renewal = new AssetWarranty;
            $renewal->fill($data);
            $renewal->asset_id = $locked->asset_id;
            $renewal->type = $locked->type;
            $renewal->is_primary = true;
            $renewal->save();

            if ($document instanceof UploadedFile) {
                $this->attachDocument($renewal, $document);
            }

            // Attached to the asset, not the new warranty row — the same
            // choice made for Assigned/Returned/Imported, so it lands in the
            // asset's own Recent Activity feed instead of only being visible
            // from a page nothing links to.
            $this->auditLogger->log(
                event: AuditEvent::Renewed,
                model: $locked->asset,
                old: ['end_date' => $locked->end_date->toDateString()],
                new: ['end_date' => $renewal->end_date->toDateString(), 'provider' => $renewal->provider],
                description: __('Renewed :type for :tag, now covered to :date', [
                    'type' => $renewal->type->label(),
                    'tag' => $locked->asset->asset_tag,
                    'date' => $renewal->end_date->format('d M Y'),
                ]),
            );

            return $renewal;
        });
    }

    public function delete(AssetWarranty $warranty): void
    {
        $warranty->delete();
    }

    /**
     * @return Collection<int, AssetWarranty>
     */
    public function expiringWithin(int $days): Collection
    {
        return AssetWarranty::query()
            ->with(['asset.category'])
            ->expiringWithin($days)
            ->orderBy('end_date')
            ->get();
    }

    /**
     * @return Collection<int, AssetWarranty>
     */
    public function expired(): Collection
    {
        return AssetWarranty::query()
            ->with(['asset.category'])
            ->expired()
            ->orderByDesc('end_date')
            ->get();
    }

    /**
     * One bucket per configured alert window, each holding only the
     * warranties expiring on *exactly* that many days from today — so a
     * daily digest mentions a given warranty once per configured window
     * (typically 30, 15 and 7 days out) rather than every day it happens to
     * fall inside one.
     *
     * @return array<int, Collection<int, AssetWarranty>>
     */
    public function digestData(): array
    {
        /** @var array<int, int> $windows */
        $windows = array_map('intval', (array) setting('warranty_alert_days', [30, 15, 7]));

        $data = [];

        foreach ($windows as $window) {
            $data[$window] = AssetWarranty::query()
                ->with(['asset.category'])
                ->primary()
                ->whereDate('end_date', now()->addDays($window)->toDateString())
                ->orderBy('end_date')
                ->get();
        }

        return $data;
    }

    private function clearPrimary(Asset $asset): void
    {
        AssetWarranty::query()->where('asset_id', $asset->id)->where('is_primary', true)
            ->update(['is_primary' => false]);
    }

    private function attachDocument(AssetWarranty $warranty, UploadedFile $document): void
    {
        $this->removeDocument($warranty);

        $path = $document->store('warranties/'.$warranty->id, 'local');

        $warranty->update([
            'document_path' => $path,
            'document_original_name' => $document->getClientOriginalName(),
            'document_mime' => $document->getMimeType(),
        ]);
    }

    private function removeDocument(AssetWarranty $warranty): void
    {
        if ($warranty->document_path !== null) {
            Storage::disk('local')->delete($warranty->document_path);
        }

        $warranty->update([
            'document_path' => null,
            'document_original_name' => null,
            'document_mime' => null,
        ]);
    }
}
