package com.pandoli365.bibimbap.service;

import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

@Component
public class FeedParser {

    public record FeedItem(String guid, String title, String link, OffsetDateTime publishedAt) {
    }

    private static final int GUID_CAP = 512;
    private static final int TITLE_CAP = 500;
    private static final int LINK_CAP = 2048;

    public List<FeedItem> parse(byte[] body) {
        if (body == null || body.length == 0) {
            return List.of();
        }
        Document doc = parseSafely(body);
        if (doc == null) {
            return List.of();
        }
        Element root = doc.getDocumentElement();
        if (root == null) {
            return List.of();
        }
        String rootName = localName(root);
        if ("feed".equalsIgnoreCase(rootName)) {
            return parseAtom(doc);
        }
        // RSS: root <rss> wraps <channel>, or some feeds expose <channel> at root.
        return parseRss(doc);
    }

    private Document parseSafely(byte[] body) {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            // XXE 방어 (보안 필수)
            dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);
            dbf.setNamespaceAware(true);
            DocumentBuilder builder = dbf.newDocumentBuilder();
            return builder.parse(new ByteArrayInputStream(body));
        } catch (Exception e) {
            return null;
        }
    }

    private List<FeedItem> parseRss(Document doc) {
        List<FeedItem> result = new ArrayList<>();
        NodeList items = doc.getElementsByTagNameNS("*", "item");
        for (int i = 0; i < items.getLength(); i++) {
            Node node = items.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            Element item = (Element) node;
            String guid = firstChildText(item, "guid");
            String link = firstChildText(item, "link");
            if (guid == null || guid.isEmpty()) {
                guid = link;
            }
            if (guid == null || guid.isEmpty()) {
                continue;
            }
            String title = firstChildText(item, "title");
            OffsetDateTime published = parseRfc1123(firstChildText(item, "pubDate"));
            result.add(new FeedItem(cap(guid, GUID_CAP), cap(title, TITLE_CAP), cap(link, LINK_CAP), published));
        }
        return result;
    }

    private List<FeedItem> parseAtom(Document doc) {
        List<FeedItem> result = new ArrayList<>();
        NodeList entries = doc.getElementsByTagNameNS("*", "entry");
        for (int i = 0; i < entries.getLength(); i++) {
            Node node = entries.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            Element entry = (Element) node;
            String guid = firstChildText(entry, "id");
            if (guid == null || guid.isEmpty()) {
                continue;
            }
            String title = firstChildText(entry, "title");
            String link = atomLink(entry);
            String dateText = firstChildText(entry, "updated");
            if (dateText == null || dateText.isEmpty()) {
                dateText = firstChildText(entry, "published");
            }
            OffsetDateTime published = parseIso(dateText);
            result.add(new FeedItem(cap(guid, GUID_CAP), cap(title, TITLE_CAP), cap(link, LINK_CAP), published));
        }
        return result;
    }

    private String atomLink(Element entry) {
        NodeList links = entry.getElementsByTagNameNS("*", "link");
        String firstHref = null;
        for (int i = 0; i < links.getLength(); i++) {
            Node node = links.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            Element link = (Element) node;
            String href = link.getAttribute("href");
            if (href == null || href.isEmpty()) {
                continue;
            }
            if (firstHref == null) {
                firstHref = href;
            }
            String rel = link.getAttribute("rel");
            if (rel == null || rel.isEmpty() || "alternate".equalsIgnoreCase(rel)) {
                return href;
            }
        }
        return firstHref;
    }

    private String firstChildText(Element parent, String childLocalName) {
        NodeList children = parent.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            if (childLocalName.equalsIgnoreCase(localName((Element) node))) {
                String text = node.getTextContent();
                return text == null ? null : text.trim();
            }
        }
        return null;
    }

    private String localName(Element element) {
        String local = element.getLocalName();
        return local != null ? local : element.getTagName();
    }

    private OffsetDateTime parseRfc1123(String value) {
        if (value == null || value.isEmpty()) {
            return null;
        }
        // 외부 피드의 pubDate 는 선행 요일이 실제 날짜와 불일치하는 경우가 흔하다
        // (예: "Tue, 24 Jun 2026" 이지만 24일은 실제 Wed). RFC_1123_DATE_TIME 은
        // day-of-week 를 검증하여 이런 입력을 DateTimeException 으로 거부한다.
        // 외부 신뢰 불가 입력이므로 선행 요일 토큰을 제거한 뒤 파싱한다(요일은 RFC_1123 에서 optional).
        String normalized = value.replaceFirst("^\\s*[A-Za-z]{3,},\\s*", "");
        try {
            // RFC_1123_DATE_TIME 은 "GMT" 등 zone name 으로 해석되어 ZonedDateTime 성격을 가진다.
            // OffsetDateTime.parse 로 직접 받으면 offset 미해석으로 DateTimeException → ZonedDateTime 경유.
            return ZonedDateTime.parse(normalized, DateTimeFormatter.RFC_1123_DATE_TIME).toOffsetDateTime();
        } catch (Exception e) {
            return null;
        }
    }

    private OffsetDateTime parseIso(String value) {
        if (value == null || value.isEmpty()) {
            return null;
        }
        try {
            return OffsetDateTime.parse(value);
        } catch (Exception e) {
            return null;
        }
    }

    private String cap(String value, int max) {
        if (value == null) {
            return null;
        }
        return value.length() <= max ? value : value.substring(0, max);
    }
}
