"""Extract content from links (web pages, PDFs, YouTube transcripts)."""
import asyncio
import ipaddress
import re
from io import BytesIO
from typing import Dict, Optional
from urllib.parse import urlparse, parse_qs

import httpx
from loguru import logger

from src.config import settings
import trafilatura
try:
    from trafilatura.metadata import extract_metadata as trafilatura_extract_metadata
except Exception:  # pragma: no cover - optional path
    trafilatura_extract_metadata = None
from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound
from pypdf import PdfReader


class LinkExtractionError(RuntimeError):
    """Raised when link extraction fails."""


class LinkExtractor:
    """Extract text content from supported link types."""

    MAX_BYTES = 2_000_000  # ~2MB
    TIMEOUT = 10.0
    MAX_REDIRECTS = 3

    async def extract(self, url: str) -> Dict[str, Optional[str]]:
        """Extract content and metadata from a link."""
        normalized = self._normalize_url(url)
        self._validate_url(normalized)

        if self._is_youtube_url(normalized):
            video_id = self._extract_youtube_id(normalized)
            if not video_id:
                raise LinkExtractionError("YouTube-Link konnte nicht erkannt werden.")
            transcript = ""
            title = ""

            if (settings.transcriptapi_key or "").strip():
                try:
                    transcript, title = await self._fetch_transcriptapi(normalized)
                except Exception as exc:
                    logger.warning(f"TranscriptAPI failed, falling back to direct fetch: {exc}")

            if not transcript:
                transcript = await self._fetch_youtube_transcript(video_id)
            if not title:
                title = await self._fetch_youtube_title(normalized)
            return {
                "source_url": normalized,
                "source_type": "youtube",
                "title": title or "YouTube Video",
                "text": transcript,
            }

        content, content_type = await self._fetch_url(normalized)
        if self._is_pdf(normalized, content_type):
            text, title = self._extract_pdf(content)
            return {
                "source_url": normalized,
                "source_type": "pdf",
                "title": title or "PDF Dokument",
                "text": text,
            }

        text, title = self._extract_html(content)
        if not text:
            raise LinkExtractionError("Konnte keinen lesbaren Text aus der Seite extrahieren.")

        return {
            "source_url": normalized,
            "source_type": "web",
            "title": title or "Webseite",
            "text": text,
        }

    def _normalize_url(self, url: str) -> str:
        url = url.strip()
        if not url:
            raise LinkExtractionError("Bitte einen Link eingeben.")
        if not re.match(r"^https?://", url, re.IGNORECASE):
            url = f"https://{url}"
        return url

    def _validate_url(self, url: str) -> None:
        parsed = urlparse(url)
        if parsed.scheme not in ("http", "https"):
            raise LinkExtractionError("Nur http/https Links sind erlaubt.")
        if not parsed.hostname:
            raise LinkExtractionError("Ungültiger Link.")

        hostname = parsed.hostname.lower()
        if hostname in {"localhost", "127.0.0.1", "0.0.0.0", "::1"}:
            raise LinkExtractionError("Lokale Links sind nicht erlaubt.")
        if hostname.endswith(".local"):
            raise LinkExtractionError("Lokale Links sind nicht erlaubt.")

        # Block private IPs if hostname is an IP literal
        try:
            ip = ipaddress.ip_address(hostname)
            if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
                raise LinkExtractionError("Lokale Links sind nicht erlaubt.")
        except ValueError:
            pass

    async def _fetch_url(self, url: str) -> tuple[bytes, str]:
        headers = {
            "User-Agent": "Mozilla/5.0 (compatible; LinkedInWorkflowBot/1.0)"
        }
        async with httpx.AsyncClient(
            follow_redirects=True,
            max_redirects=self.MAX_REDIRECTS,
            timeout=self.TIMEOUT,
            headers=headers,
            limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
        ) as client:
            response = await client.get(url)
            response.raise_for_status()
            content = response.content
            if len(content) > self.MAX_BYTES:
                raise LinkExtractionError("Die Seite ist zu groß, um verarbeitet zu werden.")
            content_type = response.headers.get("content-type", "").lower()
            return content, content_type

    def _is_pdf(self, url: str, content_type: str) -> bool:
        if "application/pdf" in content_type:
            return True
        return url.lower().endswith(".pdf")

    def _extract_pdf(self, content: bytes) -> tuple[str, Optional[str]]:
        try:
            reader = PdfReader(BytesIO(content))
            text_parts = []
            for page in reader.pages:
                page_text = page.extract_text() or ""
                if page_text:
                    text_parts.append(page_text)
            text = "\n".join(text_parts).strip()
            title = None
            try:
                title = reader.metadata.title if reader.metadata else None
            except Exception:
                title = None
            if not text:
                raise LinkExtractionError("Konnte keinen Text aus dem PDF extrahieren.")
            return text, title
        except LinkExtractionError:
            raise
        except Exception as exc:
            raise LinkExtractionError(f"PDF-Extraktion fehlgeschlagen: {exc}") from exc

    def _extract_html(self, content: bytes) -> tuple[str, Optional[str]]:
        html = content.decode("utf-8", errors="ignore")
        text = trafilatura.extract(
            html,
            include_comments=False,
            include_tables=False,
            include_formatting=False,
            output_format="txt"
        )
        title = None
        try:
            if trafilatura_extract_metadata:
                metadata = trafilatura_extract_metadata(html)
                if metadata and metadata.title:
                    title = metadata.title
        except Exception:
            title = None
        return (text or "").strip(), title

    def _is_youtube_url(self, url: str) -> bool:
        host = urlparse(url).hostname or ""
        host = host.lower()
        return "youtube.com" in host or "youtu.be" in host

    def _extract_youtube_id(self, url: str) -> Optional[str]:
        parsed = urlparse(url)
        host = (parsed.hostname or "").lower()
        if "youtu.be" in host:
            return parsed.path.strip("/").split("/")[0] or None
        if "youtube.com" in host:
            qs = parse_qs(parsed.query)
            if "v" in qs and qs["v"]:
                return qs["v"][0]
            # /shorts/{id}
            if parsed.path.startswith("/shorts/"):
                return parsed.path.split("/")[2] if len(parsed.path.split("/")) > 2 else None
        return None

    async def _fetch_youtube_transcript(self, video_id: str) -> str:
        try:
            cookies = (settings.youtube_cookies or "").strip()
            languages = ["de", "de-DE", "de-AT", "de-CH", "en", "en-US", "en-GB"]

            # Prefer full transcript list to handle generated vs. manually created captions
            transcript_list = await asyncio.to_thread(
                YouTubeTranscriptApi.list_transcripts,
                video_id,
                cookies=cookies or None
            )

            transcript = None
            try:
                transcript = transcript_list.find_manually_created_transcript(languages)
            except Exception:
                pass

            if transcript is None:
                try:
                    transcript = transcript_list.find_generated_transcript(languages)
                except Exception:
                    pass

            if transcript is None:
                try:
                    transcript = transcript_list.find_transcript(languages)
                except Exception:
                    transcript = None

            if transcript is None:
                raise LinkExtractionError("Kein passendes Transkript gefunden.")

            data = await asyncio.to_thread(transcript.fetch)
            text = " ".join([item.get("text", "") for item in data]).strip()
            if not text:
                raise LinkExtractionError("Kein Transkript verfügbar.")
            return text
        except (TranscriptsDisabled, NoTranscriptFound):
            raise LinkExtractionError("Für dieses Video ist kein Transkript verfügbar.")
        except Exception as exc:
            logger.exception(f"YouTube transcript fetch failed: {exc}")
            debug_dump = await self._debug_youtube_dump(video_id, cookies or None)
            logger.warning(f"YouTube debug dump for {video_id}:\n{debug_dump}")
            raise LinkExtractionError(
                "YouTube-Transkript konnte nicht geladen werden. "
                "YouTube blockiert den Abruf manchmal. "
                "Wenn du Zugriff hast, setze die Umgebungsvariable "
                "`YOUTUBE_COOKIES` mit gültigen Cookies."
            ) from exc

    async def _debug_youtube_dump(self, video_id: str, cookies: Optional[str]) -> str:
        urls = [
            f"https://www.youtube.com/api/timedtext?type=list&v={video_id}",
            f"https://www.youtube.com/api/timedtext?lang=de&v={video_id}",
            f"https://www.youtube.com/api/timedtext?lang=de&v={video_id}&fmt=json3",
            f"https://www.youtube.com/api/timedtext?lang=en&v={video_id}",
            f"https://www.youtube.com/watch?v={video_id}",
        ]
        headers = {
            "User-Agent": "Mozilla/5.0 (compatible; LinkedInWorkflowBot/1.0)"
        }
        if cookies:
            headers["Cookie"] = cookies

        lines = [f"cookies_used={bool(cookies)}"]
        async with httpx.AsyncClient(timeout=self.TIMEOUT, headers=headers) as client:
            for url in urls:
                try:
                    resp = await client.get(url)
                    body = resp.text
                    snippet = body[:2000].replace("\n", "\\n").replace("\r", "")
                    lines.append(f"URL: {url}")
                    lines.append(f"STATUS: {resp.status_code}")
                    lines.append(f"BODY_SNIPPET: {snippet}")
                except Exception as exc:
                    lines.append(f"URL: {url}")
                    lines.append(f"ERROR: {repr(exc)}")

        return "\n".join(lines)

    async def _fetch_transcriptapi(self, video_url: str) -> tuple[str, str]:
        api_key = (settings.transcriptapi_key or "").strip()
        if not api_key:
            raise LinkExtractionError("TranscriptAPI Key fehlt.")

        endpoint = "https://transcriptapi.com/api/v2/youtube/transcript"
        params = {
            "video_url": video_url,
            "format": "json",
            "include_timestamp": "false",
            "send_metadata": "true",
        }
        headers = {"Authorization": f"Bearer {api_key}"}

        async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
            resp = await client.get(endpoint, params=params, headers=headers)
            if resp.status_code != 200:
                raise LinkExtractionError(f"TranscriptAPI Fehler: {resp.status_code}")
            data = resp.json()

        transcript = data.get("transcript")
        text = ""
        if isinstance(transcript, list):
            text = " ".join([item.get("text", "") for item in transcript]).strip()
        elif isinstance(transcript, str):
            text = transcript.strip()

        title = ""
        meta = data.get("metadata") or {}
        if isinstance(meta, dict):
            title = meta.get("title", "") or ""

        if not text:
            raise LinkExtractionError("TranscriptAPI lieferte kein Transkript.")

        return text, title

    async def _fetch_youtube_title(self, url: str) -> Optional[str]:
        oembed = f"https://www.youtube.com/oembed?format=json&url={url}"
        try:
            async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
                response = await client.get(oembed)
                if response.status_code != 200:
                    return None
                data = response.json()
                return data.get("title")
        except Exception:
            return None
