<?php

namespace App\Services;

use App\Models\ImportedJob;
use App\Models\User;
use App\Services\Settings\SettingsService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class JobMatchingService
{
    public function __construct(private readonly SettingsService $settings) {}

    /**
     * @return Collection<int, ImportedJob>
     */
    public function rankedJobsFor(User $user): Collection
    {
        $user->loadMissing(['profile', 'questionnaireResponse', 'resumeReport']);

        if (app()->runningUnitTests()) {
            return $this->calculateRankedJobsFor($user);
        }

        $cacheKey = $this->cacheKey($user);
        $cachedPayload = Cache::get($cacheKey);

        if (is_array($cachedPayload)) {
            return $this->hydrateRankedJobs($cachedPayload);
        }

        if ($cachedPayload !== null) {
            Cache::forget($cacheKey);
        }

        $rankedJobs = $this->calculateRankedJobsFor($user);

        Cache::put($cacheKey, $this->cachePayload($rankedJobs), now()->addMinute());

        return $rankedJobs;
    }

    /**
     * @return Collection<int, ImportedJob>
     */
    private function calculateRankedJobsFor(User $user): Collection
    {
        $jobs = ImportedJob::query()
            ->active()
            ->with('source')
            ->latest('posted_at')
            ->limit(200)
            ->get();

        return $jobs
            ->filter(fn (ImportedJob $job): bool => $this->shouldIncludeJob($job))
            ->map(function (ImportedJob $job) use ($user): ImportedJob {
                $job->setAttribute('match_score', $this->score($user, $job));

                return $job;
            })
            ->filter(fn (ImportedJob $job): bool => $this->shouldShowScoredJob($job))
            ->sort(function (ImportedJob $first, ImportedJob $second): int {
                $scoreComparison = $second->match_score <=> $first->match_score;

                if ($scoreComparison !== 0 || ! $this->settings->isEnabled('matching.prioritize_recently_posted_jobs')) {
                    return $scoreComparison;
                }

                return optional($second->posted_at)->timestamp <=> optional($first->posted_at)->timestamp;
            })
            ->values();
    }

    private function cacheKey(User $user): string
    {
        $user->loadMissing(['profile', 'questionnaireResponse', 'resumeReport']);

        $activeJobVersion = ImportedJob::query()
            ->active()
            ->selectRaw('COUNT(*) as aggregate_count, UNIX_TIMESTAMP(MAX(updated_at)) as latest_update')
            ->first();

        $parts = [
            'job-match-rank-v2',
            'user:'.$user->id,
            'profile:'.($user->profile?->updated_at?->timestamp ?? 0),
            'questionnaire:'.($user->questionnaireResponse?->updated_at?->timestamp ?? 0),
            'resume:'.($user->resumeReport?->updated_at?->timestamp ?? 0),
            'jobs:'.($activeJobVersion?->aggregate_count ?? 0).':'.($activeJobVersion?->latest_update ?? 0),
            'settings:'.$this->settingsFingerprint(),
        ];

        return implode('|', $parts);
    }

    /**
     * @param  Collection<int, ImportedJob>  $jobs
     * @return array<int, array{id: int, match_score: int}>
     */
    private function cachePayload(Collection $jobs): array
    {
        return $jobs
            ->map(fn (ImportedJob $job): array => [
                'id' => (int) $job->id,
                'match_score' => (int) $job->match_score,
            ])
            ->values()
            ->all();
    }

    /**
     * @param  array<int, mixed>  $payload
     * @return Collection<int, ImportedJob>
     */
    private function hydrateRankedJobs(array $payload): Collection
    {
        $rows = collect($payload)
            ->filter(fn (mixed $row): bool => is_array($row) && isset($row['id'], $row['match_score']))
            ->values();

        if ($rows->isEmpty()) {
            return new Collection();
        }

        $jobsById = ImportedJob::query()
            ->with('source')
            ->whereIn('id', $rows->pluck('id')->map(fn (mixed $id): int => (int) $id)->all())
            ->get()
            ->keyBy('id');

        return new Collection($rows
            ->map(function (array $row) use ($jobsById): ?ImportedJob {
                $job = $jobsById->get((int) $row['id']);

                if (! $job instanceof ImportedJob) {
                    return null;
                }

                $job->setAttribute('match_score', (int) $row['match_score']);

                return $job;
            })
            ->filter()
            ->values()
            ->all());
    }

    private function settingsFingerprint(): string
    {
        return md5((string) json_encode([
            'show_jobs_without_salary' => $this->settings->get('matching.show_jobs_without_salary', true),
            'show_low_match_jobs' => $this->settings->get('matching.show_low_match_jobs', true),
            'minimum_match_score_to_show' => $this->settings->get('matching.minimum_match_score_to_show', 45),
            'prioritize_recently_posted_jobs' => $this->settings->get('matching.prioritize_recently_posted_jobs', true),
            'skill_match_weight' => $this->settings->get('matching.skill_match_weight', 35),
            'role_match_weight' => $this->settings->get('matching.role_match_weight', 20),
            'experience_match_weight' => $this->settings->get('matching.experience_match_weight', 15),
            'location_timezone_match_weight' => $this->settings->get('matching.location_timezone_match_weight', 10),
            'salary_match_weight' => $this->settings->get('matching.salary_match_weight', 10),
            'job_type_match_weight' => $this->settings->get('matching.job_type_match_weight', 5),
            'resume_strength_match_weight' => $this->settings->get('matching.resume_strength_match_weight', 5),
        ]));
    }

    public function score(User $user, ImportedJob $job): int
    {
        $profile = $user->profile;
        $questionnaire = $user->questionnaireResponse;
        $resumeReport = $user->resumeReport;

        $score = 0.0;

        $preferredCategories = $this->normalize($profile?->preferred_categories ?? $questionnaire?->preferred_categories ?? []);
        $preferredRoles = $this->normalize(array_merge(
            $profile?->preferred_roles ?? [],
            $questionnaire?->preferred_roles ?? [],
            $resumeReport?->role_suggestions ?? [],
        ));
        $skills = $this->normalize(array_merge(
            $profile?->skills ?? [],
            $questionnaire?->skills ?? [],
            $resumeReport?->extracted_skills ?? [],
        ));

        $score += in_array($this->key($job->category), $preferredCategories, true)
            ? $this->weight('role_match_weight', 20)
            : 0;

        if ($this->containsAny($job->title, $preferredRoles)) {
            $score += $this->weight('role_match_weight', 20);
        }

        $jobSkills = $this->normalize($job->skills ?? []);
        if ($jobSkills !== []) {
            $score += $this->weight('skill_match_weight', 35)
                * min(1, count(array_intersect($skills, $jobSkills)) / max(1, count($jobSkills)));
        }

        $experienceLevel = $profile?->experience_level ?: $questionnaire?->experience_level;
        if ($experienceLevel && $experienceLevel === $job->experience_level) {
            $score += $this->weight('experience_match_weight', 15);
        }

        if (
            ($profile?->country_code && $job->country_code && $profile->country_code === $job->country_code)
            || ($profile?->timezone && $job->timezone && $this->key($profile->timezone) === $this->key($job->timezone))
        ) {
            $score += $this->weight('location_timezone_match_weight', 10);
        }

        if ($this->salaryMatches($user, $job)) {
            $score += $this->weight('salary_match_weight', 10);
        }

        if (
            ($profile?->open_to_contract || $questionnaire?->open_to_contract) && $job->employment_type === 'contract'
            || ($profile?->open_to_full_time || $questionnaire?->open_to_full_time) && $job->employment_type === 'full_time'
        ) {
            $score += $this->weight('job_type_match_weight', 5);
        }

        if ($resumeReport?->ats_score || $resumeReport?->remote_readiness_score) {
            $resumeScore = (($resumeReport->ats_score ?? 0) + ($resumeReport->remote_readiness_score ?? 0)) / 2;
            $score += $this->weight('resume_strength_match_weight', 5) * min(1, max(0, $resumeScore) / 100);
        }

        return min(100, (int) round($score));
    }

    /**
     * @return array<int, string>
     */
    public function explain(User $user, ImportedJob $job): array
    {
        $user->loadMissing(['profile', 'questionnaireResponse', 'resumeReport']);

        $reasons = [];
        $profile = $user->profile;
        $questionnaire = $user->questionnaireResponse;
        $resumeReport = $user->resumeReport;
        $skills = $this->normalize(array_merge(
            $profile?->skills ?? [],
            $questionnaire?->skills ?? [],
            $resumeReport?->extracted_skills ?? [],
        ));
        $jobSkills = $this->normalize($job->skills ?? []);
        $overlap = array_values(array_intersect($skills, $jobSkills));

        if ($overlap !== []) {
            $reasons[] = 'Your profile shares relevant skills with this role: '.implode(', ', array_slice($overlap, 0, 5)).'.';
        }

        $preferredCategories = $this->normalize($profile?->preferred_categories ?? $questionnaire?->preferred_categories ?? []);
        if ($job->category && in_array($this->key($job->category), $preferredCategories, true)) {
            $reasons[] = 'The role category matches your preferred remote job category.';
        }

        $preferredRoles = $this->normalize(array_merge(
            $profile?->preferred_roles ?? [],
            $questionnaire?->preferred_roles ?? [],
            $resumeReport?->role_suggestions ?? [],
        ));
        if ($this->containsAny($job->title, $preferredRoles)) {
            $reasons[] = 'The job title aligns with your preferred or resume-suggested roles.';
        }

        $experienceLevel = $profile?->experience_level ?: $questionnaire?->experience_level;
        if ($experienceLevel && $experienceLevel === $job->experience_level) {
            $reasons[] = 'The listed experience level matches your profile.';
        }

        if (
            ($profile?->country_code && $job->country_code && $profile->country_code === $job->country_code)
            || ($profile?->timezone && $job->timezone && $this->key($profile->timezone) === $this->key($job->timezone))
        ) {
            $reasons[] = 'The location or timezone signal is compatible with your preferences.';
        }

        if ($this->salaryMatches($user, $job)) {
            $reasons[] = 'The available salary signal is compatible with your saved expectation.';
        }

        if ($resumeReport?->ats_score || $resumeReport?->remote_readiness_score) {
            $reasons[] = 'Your resume strength and remote-readiness scores contribute positively to this match.';
        }

        if ($reasons === []) {
            $reasons[] = 'This job is active and available from an approved source, but more profile or resume signals would improve match confidence.';
        }

        return $reasons;
    }

    private function shouldIncludeJob(ImportedJob $job): bool
    {
        if (! $this->settings->isEnabled('matching.show_jobs_without_salary')) {
            return $job->salary_min !== null || $job->salary_max !== null;
        }

        return true;
    }

    private function shouldShowScoredJob(ImportedJob $job): bool
    {
        if ((bool) $this->settings->get('matching.show_low_match_jobs', true)) {
            return true;
        }

        return $job->match_score >= (int) $this->settings->get('matching.minimum_match_score_to_show', 45);
    }

    private function salaryMatches(User $user, ImportedJob $job): bool
    {
        $profile = $user->profile;
        $questionnaire = $user->questionnaireResponse;
        $salaryMinimum = $profile?->salary_min ?? $questionnaire?->salary_min;
        $salaryCurrency = $profile?->salary_currency ?? $questionnaire?->salary_currency;

        if (! $salaryMinimum || ! $job->salary_max || ! $salaryCurrency || ! $job->salary_currency) {
            return false;
        }

        return strtoupper($salaryCurrency) === strtoupper($job->salary_currency)
            && $job->salary_max >= $salaryMinimum;
    }

    private function weight(string $key, int $default): int
    {
        return max(0, min(100, (int) $this->settings->get("matching.{$key}", $default)));
    }

    /**
     * @param  array<int, string>  $values
     * @return array<int, string>
     */
    private function normalize(array $values): array
    {
        return collect($values)
            ->filter()
            ->map(fn (string $value): string => $this->key($value))
            ->unique()
            ->values()
            ->all();
    }

    /**
     * @param  array<int, string>  $needles
     */
    private function containsAny(?string $haystack, array $needles): bool
    {
        if (! $haystack) {
            return false;
        }

        $haystack = $this->key($haystack);

        foreach ($needles as $needle) {
            if (Str::contains($haystack, $needle)) {
                return true;
            }
        }

        return false;
    }

    private function key(string $value): string
    {
        return Str::of($value)->lower()->squish()->replace([' ', '_'], '-')->toString();
    }
}
