"""외부 피드 URL 검증 — SSRF 차단 + redirect target 재검증

등록 시 validate_feed_url()로 1차 검증, fetch 시 redirect target마다
동일 함수로 재검증. 완전한 TOCTOU 방어는 httpx transport 레벨 후킹이
필요하므로 이 이중 검증이 현재 현실적 상한선.
"""

import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_SCHEMES = {"https"}

# HTTP 예외 도메인 — 여기에 없으면 HTTPS만 허용
# 추가 시 사유/승인일/재검토일을 주석에 기록
HTTP_EXCEPTION_DOMAINS: set[str] = {
    "www.scmp.com",  # 2026-04-13 승인, HTTPS→HTTP 301 redirect. 2026-07 재검토
}


def _is_blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
    """ipaddress 내장 속성으로 넓게 차단 (단순 대역 비교보다 안전)"""
    return (
        ip.is_private
        or ip.is_loopback
        or ip.is_link_local
        or ip.is_reserved
        or ip.is_multicast
        or ip.is_unspecified
        # Tailscale CGNAT 대역 (is_private에 포함 안 됨)
        or ip in ipaddress.ip_network("100.64.0.0/10")
    )


def validate_feed_url(url: str, allow_http: bool = False) -> str:
    """URL 검증. 실패 시 ValueError raise.

    allow_http는 HTTP_EXCEPTION_DOMAINS allowlist 연동 시에만 사용.
    API 파라미터로 노출하지 않는다.
    """
    parsed = urlparse(url)

    allowed = ALLOWED_SCHEMES | ({"http"} if allow_http else set())
    if parsed.scheme not in allowed:
        raise ValueError(f"허용되지 않은 스킴: {parsed.scheme}")

    if not parsed.hostname:
        raise ValueError("호스트명 누락")

    # DNS 해석 후 IP 차단
    try:
        addrs = socket.getaddrinfo(parsed.hostname, None)
    except socket.gaierror:
        raise ValueError(f"DNS 해석 실패: {parsed.hostname}")

    for _, _, _, _, sockaddr in addrs:
        ip = ipaddress.ip_address(sockaddr[0])
        if _is_blocked_ip(ip):
            # IP 자체를 에러에 노출하지 않음 — hostname만
            raise ValueError(f"차단된 네트워크: {parsed.hostname}")

    return url
