"""Source material ingestion for the Tea Party pipeline.

Loads text from files (single, multi, globs), stdin (explicit ``-`` or implicit
pipe detection), Cloud Locker sessions (``locker:``), DOI artifacts (``doi:``),
S3 objects (``s3://``), and URLs (``url:`` / ``https://``).
"""

from __future__ import annotations

import glob
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

import httpx
from botocore.exceptions import BotoCoreError, ClientError

from alice_cli.errors import AliceCLIError

if TYPE_CHECKING:
    from alice_cli.locker import CloudLocker
    from alice_cli.models import CLIConfig


def _is_glob_pattern(source: str) -> bool:
    """Return True if *source* contains shell glob metacharacters."""
    return any(ch in source for ch in ("*", "?", "["))


def _load_single_source(
    source: str,
    config: CLIConfig,
    locker: CloudLocker | None,
    session: Any | None,
) -> str:
    """Dispatch a single source specifier and return its text content."""
    # stdin explicit
    if source == "-":
        return sys.stdin.read()

    # locker:<id>
    if source.startswith("locker:"):
        session_id = source[len("locker:") :]
        if locker is None:
            raise AliceCLIError(f"Cloud Locker is not configured; cannot load session {session_id}")
        try:
            record = locker.load_session(session_id, "teaparty")
        except Exception as exc:
            raise AliceCLIError(f"Failed to load locker session {session_id}: {exc}") from exc
        return (record.prompt or "") + "\n\n" + (record.response or "")

    # doi:<doi>
    if source.startswith("doi:"):
        doi = source[len("doi:") :]
        raise AliceCLIError(f"DOI resolution not yet implemented for {doi}")

    # s3://<bucket>/<key>
    if source.startswith("s3://"):
        return _load_s3_source(source, session)

    # url:<url> or https://<url>
    if source.startswith("url:") or source.startswith("https://"):
        return _load_url_source(source)

    # glob pattern
    if _is_glob_pattern(source):
        return _load_glob_source(source)

    # plain file path
    return _load_file_source(source)


def _load_file_source(source: str) -> str:
    """Read a single file as UTF-8 text."""
    path = Path(source)
    if not path.exists():
        raise AliceCLIError(f"Source file not found: {source}")
    return path.read_text("utf-8")


def _load_glob_source(pattern: str) -> str:
    """Expand a glob pattern, read matching files in lexicographic order."""
    matches = sorted(glob.glob(pattern))
    if not matches:
        raise AliceCLIError(f"No files matched glob pattern: {pattern}")
    parts: list[str] = []
    for match in matches:
        path = Path(match)
        if path.is_file():
            parts.append(path.read_text("utf-8"))
    if not parts:
        raise AliceCLIError(f"No files matched glob pattern: {pattern}")
    return "\n\n".join(parts)


def _load_s3_source(source: str, session: Any | None) -> str:
    """Download an S3 object and return its UTF-8 decoded contents."""
    if session is None:
        raise AliceCLIError(f"No boto3 session available to load S3 source: {source}")
    # Parse s3://bucket/key
    without_prefix = source[len("s3://") :]
    slash_idx = without_prefix.find("/")
    if slash_idx < 1:
        raise AliceCLIError(f"Invalid S3 URI (expected s3://bucket/key): {source}")
    bucket = without_prefix[:slash_idx]
    key = without_prefix[slash_idx + 1 :]
    if not key:
        raise AliceCLIError(f"Invalid S3 URI (missing key): {source}")
    try:
        s3_client = session.client("s3")
        resp = s3_client.get_object(Bucket=bucket, Key=key)
        content: str = resp["Body"].read().decode("utf-8")
        return content
    except (ClientError, BotoCoreError) as exc:
        raise AliceCLIError(f"Failed to load S3 source {source}: {exc}") from exc


def _load_url_source(source: str) -> str:
    """Fetch a URL and return its text content, stripping HTML if needed."""
    url = source[len("url:") :] if source.startswith("url:") else source
    # Ensure the URL has a scheme
    if not url.startswith("http://") and not url.startswith("https://"):
        url = "https://" + url
    try:
        resp = httpx.get(url, follow_redirects=True, timeout=30.0)
        resp.raise_for_status()
    except httpx.HTTPError as exc:
        raise AliceCLIError(f"Failed to fetch URL {url}: {exc}") from exc
    content_type: str = resp.headers.get("content-type", "")
    if "html" in content_type.lower():
        result: str = _strip_html(resp.text)
        return result
    return str(resp.text)


def _strip_html(html: str) -> str:
    """Naively strip HTML tags to extract readable text."""
    import re

    # Remove script and style blocks
    text = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", html, flags=re.DOTALL | re.IGNORECASE)
    # Remove all remaining tags
    text = re.sub(r"<[^>]+>", " ", text)
    # Collapse whitespace
    text = re.sub(r"\s+", " ", text).strip()
    return text


def _validate_source_material(text: str) -> None:
    """Raise AliceCLIError if the combined source material is empty or whitespace-only."""
    if not text.strip():
        raise AliceCLIError("Non-empty source material is required")


def load_sources(
    sources: tuple[str, ...],
    config: CLIConfig,
    locker: CloudLocker | None,
    session: Any | None = None,
) -> str:
    """Load and concatenate source material from one or more source specifiers.

    Args:
        sources: Tuple of source specifiers — file paths, globs, ``-`` for stdin,
                 ``locker:<id>``, ``doi:<doi>``, ``s3://<bucket>/<key>``,
                 ``url:<url>``, or ``https://<url>``.
        config: CLI configuration.
        locker: Optional CloudLocker instance for locker: and doi: sources.
        session: Optional boto3 Session for S3 and URL fetching.

    Returns:
        Combined source material text (UTF-8), with double-newline separators
        between sources.

    Raises:
        AliceCLIError: If any source cannot be loaded (missing file, unknown
            session, S3 error, URL error, empty glob, empty source material).
    """
    # Implicit stdin detection: no sources and stdin is a pipe
    if not sources and not sys.stdin.isatty():
        text = sys.stdin.read()
        _validate_source_material(text)
        return text

    parts: list[str] = []
    for source in sources:
        parts.append(_load_single_source(source, config, locker, session))

    combined = "\n\n".join(parts)
    _validate_source_material(combined)
    return combined
