<?php

namespace App\Services\Jobs;

use App\Models\ImportedJob;
use App\Models\JobSource;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use SimpleXMLElement;

class XmlFeedJobImporter
{
    public function __construct(private readonly JobPayloadMapper $mapper) {}

    /**
     * @return array{imported: int, expired: int}
     */
    public function import(JobSource $source): array
    {
        if (! filled($source->feed_url)) {
            throw new RuntimeException('RSS/XML job source feed URL is not configured.');
        }

        $response = Http::timeout((int) data_get($source->sync_settings, 'timeout', 20))
            ->withHeaders($source->request_headers ?: [])
            ->get($source->feed_url, $source->query_parameters ?: []);

        if (! $response->successful()) {
            throw new RuntimeException("RSS/XML feed request failed with HTTP {$response->status()}.");
        }

        $payload = $this->xmlToArray($response->body());
        $items = $this->itemsFromPayload($source, $payload);

        if ($items === []) {
            throw new RuntimeException('RSS/XML feed did not contain any job items.');
        }

        $seenExternalIds = [];
        $imported = 0;

        DB::transaction(function () use ($source, $items, &$seenExternalIds, &$imported): void {
            foreach ($items as $item) {
                if (! is_array($item)) {
                    continue;
                }

                $normalized = $this->normalizeItem($source, $item);
                $attributes = $this->mapper->attributesFor($source, $normalized);

                if ($attributes === null) {
                    continue;
                }

                $seenExternalIds[] = $attributes['external_id'];

                ImportedJob::query()->updateOrCreate(
                    [
                        'job_source_id' => $source->id,
                        'external_id' => $attributes['external_id'],
                    ],
                    Arr::except($attributes, ['external_id'])
                );

                $imported++;
            }
        });

        return [
            'imported' => $imported,
            'expired' => $this->mapper->expireMissingJobs($source, $seenExternalIds),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function xmlToArray(string $xml): array
    {
        $document = simplexml_load_string(trim($xml), SimpleXMLElement::class, LIBXML_NONET | LIBXML_NOCDATA);

        if (! $document instanceof SimpleXMLElement) {
            throw new RuntimeException('RSS/XML feed response could not be parsed.');
        }

        $json = json_encode($document);
        $payload = is_string($json) ? json_decode($json, true) : null;

        if (! is_array($payload)) {
            throw new RuntimeException('RSS/XML feed response could not be converted into an array.');
        }

        return $payload;
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<int, mixed>
     */
    private function itemsFromPayload(JobSource $source, array $payload): array
    {
        $defaultPath = $source->type === JobSource::TYPE_RSS ? 'channel.item' : 'jobs.job';
        $itemsPath = data_get($source->sync_settings, 'item_path', $defaultPath);
        $items = $this->itemsAtPath($payload, $itemsPath);

        if ($items === [] && $itemsPath !== $defaultPath) {
            $items = $this->itemsAtPath($payload, $defaultPath);
        }

        if ($items === []) {
            foreach (['rss.channel.item', 'channel.item', 'jobs.job', 'jobs'] as $fallbackPath) {
                $items = $this->itemsAtPath($payload, $fallbackPath);

                if ($items !== []) {
                    break;
                }
            }
        }

        return $items;
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<int, mixed>
     */
    private function itemsAtPath(array $payload, mixed $itemsPath): array
    {
        $items = is_string($itemsPath) && $itemsPath !== ''
            ? data_get($payload, $itemsPath)
            : $payload;

        if (! is_array($items)) {
            return [];
        }

        if (! array_is_list($items)) {
            return [$items];
        }

        return array_values($items);
    }

    /**
     * @param  array<string, mixed>  $item
     * @return array<string, mixed>
     */
    private function normalizeItem(JobSource $source, array $item): array
    {
        $title = $this->value($item, (string) data_get($source->sync_settings, 'title_path', 'title'));
        $sourceUrl = $this->value($item, (string) data_get($source->sync_settings, 'source_url_path', 'link'));
        $applyUrl = $this->value($item, (string) data_get($source->sync_settings, 'apply_url_path', 'apply_url')) ?: $sourceUrl;
        $company = $this->value($item, (string) data_get($source->sync_settings, 'company_path', 'company'))
            ?: data_get($source->sync_settings, 'fallback_company');
        [$title, $company] = $this->resolveTitleAndCompany($source, $title, is_string($company) ? $company : null);

        return [
            'external_id' => $this->value($item, (string) data_get($source->sync_settings, 'external_id_path', 'guid')) ?: $sourceUrl,
            'title' => $title,
            'company' => $company,
            'category' => $this->value($item, (string) data_get($source->sync_settings, 'category_path', 'category')),
            'employment_type' => $this->value($item, (string) data_get($source->sync_settings, 'employment_type_path', 'employment_type')),
            'experience_level' => $this->value($item, (string) data_get($source->sync_settings, 'experience_level_path', 'experience_level')),
            'location_type' => $this->value($item, (string) data_get($source->sync_settings, 'location_type_path', 'location_type')),
            'country_code' => $this->value($item, (string) data_get($source->sync_settings, 'country_code_path', 'country_code')),
            'timezone' => $this->value($item, (string) data_get($source->sync_settings, 'location_text_path', 'region')),
            'salary_currency' => $this->value($item, (string) data_get($source->sync_settings, 'salary_currency_path', 'salary_currency')),
            'salary_min' => $this->value($item, (string) data_get($source->sync_settings, 'salary_min_path', 'salary_min')),
            'salary_max' => $this->value($item, (string) data_get($source->sync_settings, 'salary_max_path', 'salary_max')),
            'summary' => $this->value($item, (string) data_get($source->sync_settings, 'summary_path', 'description')),
            'description' => $this->value($item, (string) data_get($source->sync_settings, 'description_path', 'description')),
            'skills' => $this->value($item, (string) data_get($source->sync_settings, 'skills_path', 'skills')),
            'tags' => $this->value($item, (string) data_get($source->sync_settings, 'tags_path', 'category')),
            'source_url' => $sourceUrl,
            'apply_url' => $applyUrl,
            'source_attribution' => $source->attribution_text,
            'posted_at' => $this->value($item, (string) data_get($source->sync_settings, 'posted_at_path', 'pubDate')),
            'raw_payload' => $item,
        ];
    }

    /**
     * @return array{0: ?string, 1: ?string}
     */
    private function resolveTitleAndCompany(JobSource $source, ?string $title, ?string $company): array
    {
        if (filled($company) || ! filled($title)) {
            return [$title, $company];
        }

        $shouldSplitTitle = (bool) data_get(
            $source->sync_settings,
            'split_company_from_title',
            $this->looksLikeWeWorkRemotely($source)
        );

        if (! $shouldSplitTitle || ! str_contains($title, ':')) {
            return [$title, $company];
        }

        [$possibleCompany, $possibleTitle] = array_map('trim', explode(':', $title, 2));

        if ($possibleCompany === '' || $possibleTitle === '') {
            return [$title, $company];
        }

        return [$possibleTitle, $possibleCompany];
    }

    private function looksLikeWeWorkRemotely(JobSource $source): bool
    {
        return str((string) $source->slug)->contains('weworkremotely')
            || str((string) $source->base_url)->contains('weworkremotely.com')
            || str((string) $source->feed_url)->contains('weworkremotely.com');
    }

    private function value(array $item, string $path): mixed
    {
        if ($path === '') {
            return null;
        }

        $value = data_get($item, $path);

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

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