<?php

namespace App\Services\Documents;

use App\Models\Document;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;

class EmployeeDocumentComplianceService
{
    /**
     * @return array<string, array{label: string, aliases: array<int, string>}>
     */
    public function mandatoryCategories(): array
    {
        return config('employee_documents.mandatory', []);
    }

    /**
     * @param  array<int>  $userIds
     * @return Collection<int, EloquentCollection<int, Document>>
     */
    public function documentsGroupedByUserId(int $companyId, array $userIds): Collection
    {
        $userIds = array_values(array_unique(array_map('intval', $userIds)));
        if ($userIds === []) {
            return collect();
        }

        $employees = User::query()
            ->where('company_id', $companyId)
            ->whereIn('id', $userIds)
            ->get(['id', 'name', 'employee_number']);

        $employeesById = $employees->keyBy('id');

        $documents = Document::query()
            ->where('company_id', $companyId)
            ->get();

        $grouped = collect();
        foreach ($userIds as $userId) {
            $grouped->put($userId, new EloquentCollection());
        }

        foreach ($documents as $document) {
            $ownerIds = $this->resolveDocumentOwnerIds($document, $employeesById);

            foreach ($ownerIds as $ownerId) {
                if (! in_array($ownerId, $userIds, true)) {
                    continue;
                }

                $bucket = $grouped->get($ownerId);
                if (! $bucket->contains('id', $document->id)) {
                    $bucket->push($document);
                }
            }
        }

        return $grouped;
    }

    /**
     * @return EloquentCollection<int, Document>
     */
    public function documentsForUser(int $companyId, int $userId): EloquentCollection
    {
        return $this->documentsGroupedByUserId($companyId, [$userId])
            ->get($userId, new EloquentCollection());
    }

    /**
     * @param  EloquentCollection<int, Document>|Collection<int, Document>  $documents
     * @return array<string, true>
     */
    public function matchedMandatoryKeys(Collection $documents): array
    {
        $mandatory = $this->mandatoryCategories();
        $matched = [];

        foreach ($documents as $document) {
            foreach ($mandatory as $categoryKey => $meta) {
                if (isset($matched[$categoryKey])) {
                    continue;
                }

                if ($this->documentMatchesCategory($document, $categoryKey, $meta)) {
                    $matched[$categoryKey] = true;
                }
            }
        }

        return $matched;
    }

    /**
     * @param  EloquentCollection<int, Document>|Collection<int, Document>  $documents
     * @return array{
     *   total_required: int,
     *   completed_count: int,
     *   completed_keys: array<int, string>,
     *   missing_keys: array<int, string>,
     *   missing_labels: array<int, string>,
     *   is_compliant: bool
     * }
     */
    public function buildCompliance(Collection $documents): array
    {
        $mandatory = $this->mandatoryCategories();
        $requiredKeys = array_keys($mandatory);
        $matched = $this->matchedMandatoryKeys($documents);
        $completedKeys = array_values(array_keys($matched));
        $missingKeys = array_values(array_diff($requiredKeys, $completedKeys));
        $missingLabels = array_values(array_map(
            fn (string $key) => (string) ($mandatory[$key]['label'] ?? $key),
            $missingKeys
        ));

        return [
            'total_required' => count($requiredKeys),
            'completed_count' => count($completedKeys),
            'completed_keys' => $completedKeys,
            'missing_keys' => $missingKeys,
            'missing_labels' => $missingLabels,
            'is_compliant' => count($missingKeys) === 0,
        ];
    }

    /**
     * @param  array<int>  $visibleIds
     * @return Collection<int, array{user_id: int, user: string, missing: int, completed: int, total: int}>
     */
    public function gapsForUsers(int $companyId, array $visibleIds, int $limit = 12): Collection
    {
        $visibleIds = array_values(array_unique(array_map('intval', $visibleIds)));
        if ($visibleIds === []) {
            return collect();
        }

        $mandatory = $this->mandatoryCategories();
        $requiredKeys = array_keys($mandatory);
        $totalRequired = count($requiredKeys);

        $users = User::query()
            ->where('company_id', $companyId)
            ->forActiveDirectory()
            ->whereIn('id', $visibleIds)
            ->orderBy('name')
            ->get(['id', 'name']);

        $documentsByUser = $this->documentsGroupedByUserId($companyId, $users->pluck('id')->all());

        return $users
            ->map(function (User $user) use ($documentsByUser, $requiredKeys, $totalRequired): ?array {
                $documents = $documentsByUser->get($user->id, new EloquentCollection());
                $matched = $this->matchedMandatoryKeys($documents);
                $completed = count($matched);
                $missing = count(array_diff($requiredKeys, array_keys($matched)));

                if ($missing <= 0) {
                    return null;
                }

                return [
                    'user_id' => (int) $user->id,
                    'user' => $user->name,
                    'missing' => $missing,
                    'completed' => $completed,
                    'total' => $totalRequired,
                ];
            })
            ->filter()
            ->sort(function (array $a, array $b): int {
                if ($a['missing'] !== $b['missing']) {
                    return $b['missing'] <=> $a['missing'];
                }

                return strcmp($a['user'], $b['user']);
            })
            ->take($limit)
            ->values();
    }

    /**
     * @param  Collection<int|string, User>  $employeesById
     * @return array<int, int>
     */
    private function resolveDocumentOwnerIds(Document $document, Collection $employeesById): array
    {
        $ownerIds = [];

        $documentableId = (int) ($document->documentable_id ?? 0);
        if ($documentableId > 0 && $this->isUserMorphType($document->documentable_type)) {
            $ownerIds[] = $documentableId;
        }

        $uploadedBy = (int) ($document->uploaded_by ?? 0);
        if ($uploadedBy > 0 && ($documentableId <= 0 || $documentableId === $uploadedBy)) {
            $ownerIds[] = $uploadedBy;
        }

        $docName = mb_strtolower(trim((string) ($document->name ?? '')));

        if ($docName !== '') {
            if (preg_match('/\-\s*([A-Za-z0-9]+)\s*\-\s*\d{8,14}\s*$/u', (string) $document->name, $matches)) {
                $ref = trim($matches[1]);
                foreach ($employeesById as $employee) {
                    if ($this->employeeRefMatches($employee, $ref)) {
                        $ownerIds[] = (int) $employee->id;
                    }
                }
            }

            $sortedEmployees = $employeesById->sortByDesc(fn (User $employee) => mb_strlen(trim((string) $employee->name)));

            foreach ($sortedEmployees as $employee) {
                if ($this->employeeNameReferencedInDocument((string) $employee->name, $docName)) {
                    $ownerIds[] = (int) $employee->id;
                }
            }
        }

        return array_values(array_unique(array_filter($ownerIds)));
    }

    private function employeeNameReferencedInDocument(string $employeeName, string $docName): bool
    {
        $employeeName = mb_strtolower(trim($employeeName));
        $docName = mb_strtolower(trim($docName));

        if ($employeeName === '' || mb_strlen($employeeName) < 4 || $docName === '') {
            return false;
        }

        if (str_contains($docName, $employeeName)) {
            return true;
        }

        $parts = array_values(array_filter(preg_split('/\s+/u', $employeeName) ?: []));
        if (count($parts) >= 2) {
            $reversed = implode(' ', array_reverse($parts));
            if (str_contains($docName, $reversed)) {
                return true;
            }

            if (collect($parts)->every(fn (string $part) => mb_strlen($part) >= 3 && str_contains($docName, $part))) {
                return true;
            }
        }

        return false;
    }

    private function employeeRefMatches(User $employee, string $ref): bool
    {
        $ref = trim($ref);
        if ($ref === '') {
            return false;
        }

        $employeeNumber = trim((string) ($employee->employee_number ?? ''));
        if ($employeeNumber !== '' && strcasecmp($employeeNumber, $ref) === 0) {
            return true;
        }

        return strcasecmp('EMP' . $employee->id, $ref) === 0;
    }

    private function isUserMorphType(?string $type): bool
    {
        if ($type === null || $type === '') {
            return false;
        }

        $userClass = User::class;
        if ($type === $userClass) {
            return true;
        }

        $alias = Relation::getMorphAlias($userClass);

        return $alias !== null && $type === $alias;
    }

    /**
     * @param  array{label: string, aliases: array<int, string>}  $meta
     */
    private function documentMatchesCategory(Document $document, string $categoryKey, array $meta): bool
    {
        $type = mb_strtolower(trim((string) ($document->type ?? '')));
        $category = mb_strtolower(trim((string) ($document->category ?? '')));
        $normalizedKey = mb_strtolower(trim($categoryKey));

        if ($type === $normalizedKey || $category === $normalizedKey) {
            return true;
        }

        $aliases = collect($meta['aliases'] ?? [])
            ->map(fn ($alias) => mb_strtolower(trim((string) $alias)))
            ->filter()
            ->all();

        $categoryTerms = array_values(array_unique(array_filter([
            $normalizedKey,
            mb_strtolower(trim((string) ($meta['label'] ?? ''))),
            ...$aliases,
        ])));

        foreach ($categoryTerms as $term) {
            if ($this->documentFieldMatchesTerm($document, $term)) {
                return true;
            }
        }

        return false;
    }

    private function documentFieldMatchesTerm(Document $document, string $term): bool
    {
        if ($term === '') {
            return false;
        }

        $fields = [
            mb_strtolower(trim((string) ($document->type ?? ''))),
            mb_strtolower(trim((string) ($document->category ?? ''))),
            mb_strtolower(trim((string) ($document->name ?? ''))),
        ];

        foreach ($fields as $field) {
            if ($field === $term || ($field !== '' && str_contains($field, $term))) {
                return true;
            }
        }

        return false;
    }
}
