<?php

namespace App\Services\Jobs;

use App\Models\ImportedJob;
use App\Models\JobSource;
use App\Support\JobText;
use Illuminate\Support\Carbon;

class JobPayloadMapper
{
    /**
     * @param  array<string, mixed>  $item
     * @return array<string, mixed>|null
     */
    public function attributesFor(JobSource $source, array $item): ?array
    {
        $title = $this->stringFrom($source, $item, 'title_path', ['title', 'position', 'role', 'jobTitle']);
        $company = $this->stringFrom($source, $item, 'company_path', ['company', 'company_name', 'organization', 'companyName']);
        $sourceUrl = $this->urlFrom($source, $item, 'source_url_path', ['source_url', 'url', 'job_url']);
        $applyUrl = $this->urlFrom($source, $item, 'apply_url_path', ['apply_url', 'application_url', 'apply', 'url']);

        if (! $title || ! $company || ! $sourceUrl || ! $applyUrl) {
            return null;
        }

        $externalId = $this->stringFrom($source, $item, 'external_id_path', ['external_id', 'id', 'guid', 'slug', 'jobSlug'])
            ?: sha1($sourceUrl.'|'.$applyUrl.'|'.$title.'|'.$company);

        $rawSummary = $this->nullableStringFrom($source, $item, 'summary_path', ['summary', 'excerpt', 'jobExcerpt']);
        $rawDescription = $this->nullableStringFrom($source, $item, 'description_path', ['description', 'body', 'jobDescription']);
        $salaryCurrency = $this->currency($this->stringFrom($source, $item, 'salary_currency_path', ['salary_currency', 'currency', 'salaryCurrency']));
        $salaryMin = $this->integerFrom($source, $item, 'salary_min_path', ['salary_min', 'min_salary', 'annualSalaryMin', 'salaryMin']);
        $salaryMax = $this->integerFrom($source, $item, 'salary_max_path', ['salary_max', 'max_salary', 'annualSalaryMax', 'salaryMax']);

        if ($salaryMin === null && $salaryMax === null) {
            $parsedSalary = JobText::salaryRange(($rawSummary ?? '')."\n".($rawDescription ?? ''));
            $salaryCurrency = $salaryCurrency ?: $parsedSalary['currency'];
            $salaryMin = $parsedSalary['min'];
            $salaryMax = $parsedSalary['max'];
        }

        return [
            'external_id' => str($externalId)->limit(255, '')->toString(),
            'title' => str($title)->limit(255, '')->toString(),
            'company' => str($company)->limit(255, '')->toString(),
            'category' => $this->category($this->stringFrom($source, $item, 'category_path', ['category', 'department', 'function', 'jobIndustry'])),
            'employment_type' => $this->employmentType($this->stringFrom($source, $item, 'employment_type_path', ['employment_type', 'type', 'job_type', 'jobType', 'job_types'])),
            'experience_level' => $this->experienceLevel($this->stringFrom($source, $item, 'experience_level_path', ['experience_level', 'seniority'])),
            'location_type' => $this->locationType($this->stringFrom($source, $item, 'location_type_path', ['location_type', 'workplace', 'remote'])),
            'country_code' => $this->countryCode($this->stringFrom($source, $item, 'country_code_path', ['country_code', 'country'])),
            'timezone' => $this->nullableStringFrom($source, $item, 'location_text_path', ['timezone', 'jobGeo', 'location', 'candidate_required_location']),
            'salary_currency' => $salaryCurrency,
            'salary_min' => $salaryMin,
            'salary_max' => $salaryMax,
            'summary' => JobText::excerpt($rawSummary ?: $rawDescription, 520),
            'description' => JobText::clean($rawDescription ?: $rawSummary),
            'skills' => $this->arrayOfStringsFrom($source, $item, 'skills_path', ['skills', 'jobSkills']),
            'tags' => $this->arrayOfStringsFrom($source, $item, 'tags_path', ['tags', 'jobTags', 'jobIndustry', 'category']),
            'source_url' => $sourceUrl,
            'apply_url' => $applyUrl,
            'source_attribution' => $this->nullableStringFrom($source, $item, 'source_attribution_path', ['source_attribution', 'attribution']) ?: $source->attribution_text,
            'status' => $this->status($this->string($item, ['status'])),
            'is_featured' => (bool) data_get($item, 'is_featured', false),
            'posted_at' => $this->dateFrom($source, $item, 'posted_at_path', ['posted_at', 'published_at', 'created_at', 'publication_date', 'pubDate']),
            'expires_at' => $this->dateFrom($source, $item, 'expires_at_path', ['expires_at', 'valid_until']),
            'last_seen_at' => now(),
            'raw_payload' => $item,
        ];
    }

    /**
     * @param  array<int, string>  $seenExternalIds
     */
    public function expireMissingJobs(JobSource $source, array $seenExternalIds): int
    {
        if (! (bool) data_get($source->sync_settings, 'expire_missing', false)) {
            return 0;
        }

        return ImportedJob::query()
            ->where('job_source_id', $source->id)
            ->whereNotIn('external_id', $seenExternalIds)
            ->where('status', ImportedJob::STATUS_ACTIVE)
            ->update([
                'status' => ImportedJob::STATUS_EXPIRED,
                'expires_at' => now(),
            ]);
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function stringFrom(JobSource $source, array $item, string $settingKey, array $keys): ?string
    {
        $configuredPath = data_get($source->sync_settings, $settingKey);

        if (is_string($configuredPath) && $configuredPath !== '') {
            $value = $this->valueAtPath($item, $configuredPath);

            if ($value !== null) {
                return $value;
            }
        }

        return $this->string($item, $keys);
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function nullableStringFrom(JobSource $source, array $item, string $settingKey, array $keys): ?string
    {
        $value = $this->stringFrom($source, $item, $settingKey, $keys);

        return $value ? str($value)->limit(4000)->toString() : null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function urlFrom(JobSource $source, array $item, string $settingKey, array $keys): ?string
    {
        $value = $this->stringFrom($source, $item, $settingKey, $keys);

        return $value && filter_var($value, FILTER_VALIDATE_URL) ? $value : null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function integerFrom(JobSource $source, array $item, string $settingKey, array $keys): ?int
    {
        $configuredPath = data_get($source->sync_settings, $settingKey);

        if (is_string($configuredPath) && $configuredPath !== '') {
            $value = data_get($item, $configuredPath);

            if (is_numeric($value)) {
                return max(0, (int) $value);
            }
        }

        return $this->integer($item, $keys);
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function dateFrom(JobSource $source, array $item, string $settingKey, array $keys): ?Carbon
    {
        $configuredPath = data_get($source->sync_settings, $settingKey);

        if (is_string($configuredPath) && $configuredPath !== '') {
            $value = $this->valueAtPath($item, $configuredPath);

            if ($value !== null) {
                try {
                    return Carbon::parse($value);
                } catch (\Throwable) {
                    return null;
                }
            }
        }

        return $this->date($item, $keys);
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     * @return array<int, string>|null
     */
    private function arrayOfStringsFrom(JobSource $source, array $item, string $settingKey, array $keys): ?array
    {
        $configuredPath = data_get($source->sync_settings, $settingKey);

        if (is_string($configuredPath) && $configuredPath !== '') {
            $value = data_get($item, $configuredPath);
            $strings = $this->stringsFromValue($value);

            if ($strings !== null) {
                return $strings;
            }
        }

        return $this->arrayOfStrings($item, $keys);
    }

    /**
     * @param  array<string, mixed>  $item
     */
    private function valueAtPath(array $item, string $path): ?string
    {
        $value = data_get($item, $path);

        if (is_array($value)) {
            return collect($value)
                ->flatten()
                ->map(fn (mixed $item): string => trim((string) $item))
                ->filter()
                ->implode(', ') ?: null;
        }

        return is_scalar($value) && filled((string) $value) ? trim((string) $value) : null;
    }

    /**
     * @return array<int, string>|null
     */
    private function stringsFromValue(mixed $value): ?array
    {
        if (is_string($value)) {
            return collect(explode(',', $value))
                ->map(fn (string $item): string => trim($item))
                ->filter()
                ->values()
                ->all();
        }

        if (is_array($value)) {
            return collect($value)
                ->flatten()
                ->map(fn (mixed $item): string => trim((string) $item))
                ->filter()
                ->values()
                ->all();
        }

        return null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function string(array $item, array $keys): ?string
    {
        foreach ($keys as $key) {
            $value = $this->valueAtPath($item, $key);

            if ($value !== null) {
                return $value;
            }
        }

        return null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function nullableString(array $item, array $keys): ?string
    {
        $value = $this->string($item, $keys);

        return $value ? str($value)->limit(4000)->toString() : null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function url(array $item, array $keys): ?string
    {
        $value = $this->string($item, $keys);

        return $value && filter_var($value, FILTER_VALIDATE_URL) ? $value : null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function integer(array $item, array $keys): ?int
    {
        foreach ($keys as $key) {
            $value = data_get($item, $key);

            if (is_numeric($value)) {
                return max(0, (int) $value);
            }
        }

        return null;
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     */
    private function date(array $item, array $keys): ?Carbon
    {
        $value = $this->string($item, $keys);

        if (! $value) {
            return null;
        }

        try {
            return Carbon::parse($value);
        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * @param  array<string, mixed>  $item
     * @param  array<int, string>  $keys
     * @return array<int, string>|null
     */
    private function arrayOfStrings(array $item, array $keys): ?array
    {
        foreach ($keys as $key) {
            $value = data_get($item, $key);
            $strings = $this->stringsFromValue($value);

            if ($strings !== null) {
                return $strings;
            }
        }

        return null;
    }

    private function category(?string $value): string
    {
        $normalized = str($value ?: 'general')->lower()->replace(['_', ' '], '-')->toString();
        $allowed = [
            'tech',
            'design',
            'marketing',
            'sales',
            'writing',
            'support',
            'product',
            'operations',
            'virtual-assistant',
            'finance',
            'hr',
            'data',
            'ai',
            'general',
        ];

        if (in_array($normalized, $allowed, true)) {
            return $normalized;
        }

        foreach ($allowed as $category) {
            if ($category !== 'general' && str_contains($normalized, $category)) {
                return $category;
            }
        }

        return 'general';
    }

    private function employmentType(?string $value): ?string
    {
        if (! $value) {
            return null;
        }

        return match (str($value)->lower()->replace(['-', ' '], '_')->toString()) {
            'full_time', 'fulltime' => 'full_time',
            'part_time', 'parttime' => 'part_time',
            'contract' => 'contract',
            'freelance' => 'freelance',
            default => null,
        };
    }

    private function experienceLevel(?string $value): ?string
    {
        if (! $value) {
            return null;
        }

        return match (str($value)->lower()->replace(['-', ' '], '_')->toString()) {
            'entry', 'junior', 'mid', 'senior', 'lead', 'manager', 'executive' => str($value)->lower()->replace(['-', ' '], '_')->toString(),
            default => null,
        };
    }

    private function locationType(?string $value): string
    {
        if (! $value) {
            return 'remote';
        }

        return str_contains(strtolower($value), 'hybrid') ? 'hybrid' : 'remote';
    }

    private function countryCode(?string $value): ?string
    {
        return $value ? str($value)->upper()->limit(2, '')->toString() : null;
    }

    private function currency(?string $value): ?string
    {
        return $value ? str($value)->upper()->limit(3, '')->toString() : null;
    }

    private function status(?string $value): string
    {
        return match (str($value ?: ImportedJob::STATUS_ACTIVE)->lower()->toString()) {
            ImportedJob::STATUS_INACTIVE => ImportedJob::STATUS_INACTIVE,
            ImportedJob::STATUS_EXPIRED => ImportedJob::STATUS_EXPIRED,
            default => ImportedJob::STATUS_ACTIVE,
        };
    }
}
