<?php

namespace App\Domain\Documents\Services;

use App\Domain\Audit\Enums\ActivityCategory;
use App\Domain\Audit\Services\ActivityLogger;
use App\Domain\Documents\Enums\DocumentStatus;
use App\Domain\Documents\Models\Document;
use App\Domain\Documents\Models\DocumentAccessLog;
use App\Domain\Documents\Models\DocumentType;
use App\Domain\Identity\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class DocumentService
{
    public function __construct(
        private readonly DocumentStorage $storage,
        private readonly ActivityLogger $activity,
    ) {}

    /**
     * Stores an upload as the current version of its type for this owner.
     *
     * A previous version is superseded rather than replaced: the old row and
     * its file stay for audit (§17A "Document Versioning").
     *
     * @param  array<string, mixed>  $attributes
     * @param  bool  $enforceMetadata  Whether the type's required fields must be
     *                                 present. Public, pre-review attachments
     *                                 set this false: an employer attaching a
     *                                 demand letter to an enquiry does not yet
     *                                 have its reference number, and the
     *                                 verification step catches what is missing.
     *
     * @throws ValidationException
     */
    public function upload(
        Model $owner,
        DocumentType $type,
        UploadedFile $file,
        array $attributes = [],
        ?User $actor = null,
        bool $enforceMetadata = true,
    ): Document {
        if ($enforceMetadata) {
            $this->assertMetadataComplete($type, $attributes);
        }

        $stored = $this->storage->store($file, $type, (string) $owner->getKey());

        return DB::transaction(function () use ($owner, $type, $stored, $attributes, $actor) {
            $previous = $type->allows_multiple
                ? null
                : Document::query()
                    ->where('owner_type', $owner->getMorphClass())
                    ->where('owner_id', $owner->getKey())
                    ->where('document_type_id', $type->getKey())
                    ->current()
                    ->lockForUpdate()
                    ->first();

            if ($previous !== null) {
                $previous->update(['is_current' => false]);
            }

            $document = Document::create([
                'company_id' => $owner->company_id,
                'document_type_id' => $type->getKey(),
                'owner_type' => $owner->getMorphClass(),
                'owner_id' => $owner->getKey(),
                'file_path' => $stored['path'],
                'original_name' => $stored['original_name'],
                'mime_type' => $stored['mime'],
                'size_bytes' => $stored['size'],
                'checksum' => $stored['checksum'],
                'document_number' => $attributes['document_number'] ?? null,
                'issue_date' => $attributes['issue_date'] ?? null,
                'expiry_date' => $attributes['expiry_date'] ?? null,
                'issuing_country' => $attributes['issuing_country'] ?? null,
                'issuer' => $attributes['issuer'] ?? null,
                // A fresh upload always re-enters review, even after a rejection.
                'verification_status' => DocumentStatus::Uploaded->value,
                'version' => ($previous?->version ?? 0) + 1,
                'replaces_document_id' => $previous?->getKey(),
                'is_current' => true,
                'uploaded_by' => $actor?->getKey(),
            ]);

            $this->activity->log(ActivityCategory::Document, 'document.uploaded', [
                'subject' => $document,
                'parent' => $owner,
                'actor' => $actor,
                'title' => $document->version > 1
                    ? "New version of {$type->name} uploaded"
                    : "{$type->name} uploaded",
                'description' => $document->version > 1
                    ? "Version {$document->version} replaced version {$previous?->version}."
                    : $stored['original_name'],
                'visibility' => 'shared',
            ]);

            return $document;
        });
    }

    /** @throws ValidationException */
    public function verify(Document $document, User $actor, ?string $notes = null): Document
    {
        $this->assertAwaitingDecision($document);

        if ($document->hasExpired()) {
            throw ValidationException::withMessages([
                'expiry_date' => ['This document has already expired and cannot be verified.'],
            ]);
        }

        return DB::transaction(function () use ($document, $actor, $notes) {
            $document->update([
                'verification_status' => DocumentStatus::Verified->value,
                'verified_by' => $actor->getKey(),
                'verified_at' => now(),
                'rejection_reason' => null,
                'internal_notes' => $notes ?? $document->internal_notes,
            ]);

            $this->activity->log(ActivityCategory::Document, 'document.verified', [
                'subject' => $document,
                'parent' => $document->owner,
                'actor' => $actor,
                'title' => ($document->documentType?->name ?? 'Document').' verified',
                'description' => $document->document_number
                    ? 'Reference '.$document->document_number
                    : null,
                'visibility' => 'shared',
            ]);

            return $document;
        });
    }

    /**
     * Rejects a document, either outright or asking for a replacement.
     *
     * @throws ValidationException
     */
    public function reject(
        Document $document,
        User $actor,
        string $reason,
        bool $requestReupload = true,
    ): Document {
        $this->assertAwaitingDecision($document);

        if (blank($reason)) {
            throw ValidationException::withMessages([
                'reason' => ['A rejection must say what is wrong with the document.'],
            ]);
        }

        return DB::transaction(function () use ($document, $actor, $reason, $requestReupload) {
            $document->update([
                'verification_status' => $requestReupload
                    ? DocumentStatus::ReuploadRequired->value
                    : DocumentStatus::Rejected->value,
                'verified_by' => $actor->getKey(),
                'verified_at' => now(),
                'rejection_reason' => $reason,
            ]);

            $this->activity->log(ActivityCategory::Document, 'document.rejected', [
                'subject' => $document,
                'parent' => $document->owner,
                'actor' => $actor,
                'title' => $requestReupload
                    ? 'Replacement requested for '.($document->documentType?->name ?? 'document')
                    : ($document->documentType?->name ?? 'Document').' rejected',
                // The reason is written for the applicant to act on.
                'description' => $reason,
                'visibility' => 'shared',
            ]);

            return $document;
        });
    }

    /** Excuses a required document, with a reason (§17A). */
    public function waive(Document|DocumentType $subject, Model $owner, User $actor, string $reason): Document
    {
        if (blank($reason)) {
            throw ValidationException::withMessages([
                'reason' => ['Waiving a required document needs a reason.'],
            ]);
        }

        if ($subject instanceof Document) {
            $subject->update([
                'verification_status' => DocumentStatus::Waived->value,
                'verified_by' => $actor->getKey(),
                'verified_at' => now(),
                'rejection_reason' => $reason,
            ]);

            return $subject;
        }

        // Nothing was ever uploaded: record the waiver as a metadata-only row.
        return Document::create([
            'company_id' => $owner->company_id,
            'document_type_id' => $subject->getKey(),
            'owner_type' => $owner->getMorphClass(),
            'owner_id' => $owner->getKey(),
            'file_path' => '',
            'original_name' => '',
            'mime_type' => 'application/octet-stream',
            'size_bytes' => 0,
            'checksum' => '',
            'verification_status' => DocumentStatus::Waived->value,
            'rejection_reason' => $reason,
            'verified_by' => $actor->getKey(),
            'verified_at' => now(),
            'version' => 1,
            'is_current' => true,
            'uploaded_by' => $actor->getKey(),
        ]);
    }

    public function logAccess(Document $document, ?User $user, string $action, ?string $ip, ?string $agent): void
    {
        // Downloads of sensitive documents are an audit event, not just a
        // counter: who read someone's passport scan matters (§65).
        if ($action === 'download' && $document->documentType?->is_sensitive) {
            $this->activity->log(ActivityCategory::Security, 'document.downloaded', [
                'subject' => $document,
                'parent' => $document->owner,
                'actor' => $user,
                'title' => 'Sensitive document downloaded',
                'description' => $document->documentType?->name,
            ]);
        }

        DocumentAccessLog::create([
            'document_id' => $document->getKey(),
            'user_id' => $user?->getKey(),
            'action' => $action,
            'ip_address' => $ip,
            'user_agent' => $agent ? mb_substr($agent, 0, 512) : null,
            'accessed_at' => now(),
        ]);
    }

    /** @throws ValidationException */
    private function assertAwaitingDecision(Document $document): void
    {
        if (! $document->is_current) {
            throw ValidationException::withMessages([
                'document' => ['Only the current version of a document can be reviewed.'],
            ]);
        }

        if ($document->verification_status === DocumentStatus::Verified) {
            throw ValidationException::withMessages([
                'document' => ['This document has already been verified.'],
            ]);
        }
    }

    /** @throws ValidationException */
    private function assertMetadataComplete(DocumentType $type, array $attributes): void
    {
        $errors = [];

        if ($type->requires_number && blank($attributes['document_number'] ?? null)) {
            $errors['document_number'] = ["A {$type->name} needs its document number."];
        }

        if ($type->requires_issue_date && blank($attributes['issue_date'] ?? null)) {
            $errors['issue_date'] = ["A {$type->name} needs its issue date."];
        }

        if ($type->requires_expiry_date && blank($attributes['expiry_date'] ?? null)) {
            $errors['expiry_date'] = ["A {$type->name} needs its expiry date."];
        }

        if ($errors !== []) {
            throw ValidationException::withMessages($errors);
        }
    }
}
