<?php

declare(strict_types=1);

namespace App\Services\Rss;

use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Illuminate\Support\Str;
use InvalidArgumentException;

final class RssFeedParser
{
    /**
     * Parse RSS 2.0 or Atom XML into normalized rows.
     *
     * @return list<array{remote_id: string, title: string, url: string, summary: string|null, published_at: CarbonInterface|null}>
     */
    public static function parse(string $xml): array
    {
        $xml = trim($xml);
        if ($xml === '') {
            throw new InvalidArgumentException('Empty RSS/XML body.');
        }

        $dom = new DOMDocument;
        $loaded = @$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
        if (! $loaded) {
            throw new InvalidArgumentException('Invalid XML.');
        }

        $xpath = new DOMXPath($dom);
        $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');

        $atomEntries = $xpath->query('//atom:entry');
        if ($atomEntries !== false && $atomEntries->length > 0) {
            return self::parseAtomEntries($atomEntries);
        }

        $rssItems = $xpath->query('//item');
        if ($rssItems !== false && $rssItems->length > 0) {
            return self::parseRssItems($rssItems);
        }

        throw new InvalidArgumentException('No RSS items or Atom entries found.');
    }

    /**
     * @param  \DOMNodeList<DOMElement>  $items
     * @return list<array{remote_id: string, title: string, url: string, summary: string|null, published_at: CarbonInterface|null}>
     */
    private static function parseRssItems(\DOMNodeList $items): array
    {
        $out = [];
        foreach ($items as $item) {
            if (! $item instanceof DOMElement) {
                continue;
            }

            $title = self::childText($item, 'title');
            $link = self::childText($item, 'link');
            $guid = self::childText($item, 'guid');
            $pub = self::childText($item, 'pubDate');
            $description = self::childText($item, 'description')
                ?: self::childText($item, 'summary');

            $url = trim($link);
            if ($url === '') {
                continue;
            }

            $remoteId = trim($guid) !== '' ? trim($guid) : md5($url);
            $title = $title !== '' ? Str::limit(strip_tags(html_entity_decode($title)), 500) : Str::limit($url, 120);

            $out[] = [
                'remote_id' => Str::limit($remoteId, 500, ''),
                'title' => $title,
                'url' => Str::limit($url, 2000, ''),
                'summary' => self::normalizeSummary($description),
                'published_at' => self::parseDate($pub),
            ];
        }

        return $out;
    }

    /**
     * @param  \DOMNodeList<DOMElement>  $entries
     * @return list<array{remote_id: string, title: string, url: string, summary: string|null, published_at: CarbonInterface|null}>
     */
    private static function parseAtomEntries(\DOMNodeList $entries): array
    {
        $out = [];
        foreach ($entries as $entry) {
            if (! $entry instanceof DOMElement) {
                continue;
            }

            $title = self::childText($entry, 'title');
            $id = self::childText($entry, 'id');
            $updated = self::childText($entry, 'updated');
            $published = self::childText($entry, 'published');
            $summary = self::childText($entry, 'summary')
                ?: self::childText($entry, 'content');

            $url = self::atomLinkHref($entry);
            if ($url === '') {
                continue;
            }

            $remoteId = trim($id) !== '' ? trim($id) : md5($url);
            $title = $title !== '' ? Str::limit(strip_tags(html_entity_decode($title)), 500) : Str::limit($url, 120);

            $dateRaw = $published !== '' ? $published : $updated;

            $out[] = [
                'remote_id' => Str::limit($remoteId, 500, ''),
                'title' => $title,
                'url' => Str::limit($url, 2000, ''),
                'summary' => self::normalizeSummary($summary),
                'published_at' => self::parseDate($dateRaw),
            ];
        }

        return $out;
    }

    private static function childText(DOMElement $parent, string $localName): string
    {
        foreach ($parent->childNodes as $child) {
            if ($child instanceof DOMElement && $child->localName === $localName) {
                return trim($child->textContent);
            }
        }

        return '';
    }

    private static function atomLinkHref(DOMElement $entry): string
    {
        $fallback = '';
        foreach ($entry->getElementsByTagNameNS('*', 'link') as $link) {
            if (! $link instanceof DOMElement) {
                continue;
            }
            $href = trim($link->getAttribute('href'));
            if ($href === '') {
                continue;
            }
            $rel = strtolower(trim($link->getAttribute('rel')));
            if ($rel === '' || $rel === 'alternate') {
                return $href;
            }
            if ($fallback === '') {
                $fallback = $href;
            }
        }

        return $fallback;
    }

    private static function normalizeSummary(string $raw): ?string
    {
        $plain = trim(preg_replace('/\s+/', ' ', strip_tags(html_entity_decode($raw))) ?? '');
        if ($plain === '') {
            return null;
        }

        return Str::limit($plain, 320, '…');
    }

    private static function parseDate(string $raw): ?CarbonInterface
    {
        $raw = trim($raw);
        if ($raw === '') {
            return null;
        }

        try {
            return CarbonImmutable::parse($raw);
        } catch (\Throwable) {
            return null;
        }
    }
}
