"""Shared non-transport helpers."""

from __future__ import annotations

import re
import urllib.parse
from hashlib import sha1
from pathlib import Path
from typing import Any
from collections.abc import Iterable
import contextlib


def first_non_empty(*values: Any) -> Any:
    for value in values:
        if value not in (None, "", [], {}):
            return value
    return None


def flatten_url_candidates(value: Any) -> list[str]:
    candidates: list[str] = []

    def visit(node: Any) -> None:
        if isinstance(node, str):
            normalized = normalize_text(node)
            if is_http_url(normalized):
                candidates.append(normalized)
            return
        if isinstance(node, list):
            for item in node:
                visit(item)
            return
        if isinstance(node, dict):
            for key in ("@href", "href", "url", "URL", "@value", "value"):
                if key in node:
                    visit(node.get(key))

    visit(value)
    deduped: list[str] = []
    seen: set[str] = set()
    for candidate in candidates:
        if candidate not in seen:
            seen.add(candidate)
            deduped.append(candidate)
    return deduped


def is_http_url(url: str | None) -> bool:
    normalized = normalize_text(url)
    if not normalized:
        return False
    parsed = urllib.parse.urlparse(normalized)
    return parsed.scheme in {"http", "https"} and bool(parsed.netloc)


def is_api_like_url(url: str | None) -> bool:
    normalized = normalize_text(url)
    if not normalized:
        return False
    parsed = urllib.parse.urlparse(normalized)
    hostname = parsed.netloc.lower()
    path = parsed.path.lower()
    if not hostname:
        return False
    from .provider_catalog import is_declared_api_host

    if is_declared_api_host(hostname):
        return True
    if hostname.startswith("api."):
        return True
    return any(
        token in path
        for token in (
            "/content/abstract/",
            "/content/article/",
            "/inward/record.uri",
            "/inward/citedby.uri",
        )
    )


def choose_public_landing_page_url(*values: Any) -> str | None:
    candidates: list[str] = []
    seen: set[str] = set()
    for value in values:
        for candidate in flatten_url_candidates(value):
            if candidate not in seen:
                seen.add(candidate)
                candidates.append(candidate)
    for candidate in candidates:
        if not is_api_like_url(candidate):
            return candidate
    return candidates[0] if candidates else None


def normalize_text(value: str | None) -> str:
    text = (value or "").replace("\xa0", " ")
    text = re.sub(r"[ \t\r\f\v]+", " ", text)
    text = re.sub(r"\s*\n\s*", "\n", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


def safe_text(value: Any) -> str:
    return normalize_text(str(value or ""))


def extend_unique(target: list[str], items: list[str] | None) -> None:
    for item in items or []:
        normalized = normalize_text(item)
        if normalized and normalized not in target:
            target.append(normalized)


def dedupe_normalized(values: Iterable[str | None] | None) -> list[str]:
    """Return non-empty, order-preserving, normalized-unique strings."""
    seen: set[str] = set()
    ordered: list[str] = []
    for value in values or []:
        normalized = normalize_text(value)
        if normalized and normalized not in seen:
            seen.add(normalized)
            ordered.append(normalized)
    return ordered


def strip_html_tags(value: str | None) -> str | None:
    if not value:
        return value
    text = re.sub(r"<[^>]+>", " ", value)
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def first_list_item(value: Any) -> Any:
    if isinstance(value, list):
        return value[0] if value else None
    return value


def date_parts_to_string(node: Any) -> str | None:
    if not isinstance(node, dict):
        return None
    parts = node.get("date-parts")
    if not isinstance(parts, list) or not parts:
        return None
    first = parts[0]
    if not isinstance(first, list) or not first:
        return None
    return "-".join(str(part) for part in first)


def sanitize_filename(value: str) -> str:
    original = value or ""
    digest = sha1(original.encode("utf-8", errors="ignore")).hexdigest()[:8]
    filename = re.sub(r"[^A-Za-z0-9._-]+", "_", original)
    filename = re.sub(r"_+", "_", filename).strip("._-")
    if not filename:
        return f"fulltext_{digest}"

    max_length = 180
    if len(filename) <= max_length:
        return filename

    suffix = f"_{digest}"
    truncated = filename[:max_length].rstrip("._-")
    truncated = truncated[: max(1, max_length - len(suffix))].rstrip("._-")
    return f"{truncated or 'fulltext'}{suffix}"


def _extract_last_name(author: str) -> str:
    name = (author or "").strip()
    if "," in name:
        return name.split(",")[0].strip()
    parts = name.split()
    return parts[-1] if parts else ""


def _extract_year(published: str | None) -> str | None:
    if not published:
        return None
    m = re.match(r"(\d{4})", published.strip())
    return m.group(1) if m else None


def _sanitize_author_component(name: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", name)
    cleaned = re.sub(r"_+", "_", cleaned).strip("._-")
    return cleaned


def format_paper_stem(
    authors: list[str] | None,
    year: str | None,
    title: str | None,
    doi: str | None = None,
) -> str:
    if authors:
        last = _sanitize_author_component(_extract_last_name(authors[0])) or "unknown"
        author_part = f"{last}_et_al" if len(authors) > 1 else last
    else:
        author_part = "unknown"
    year_part = year or "unknown"
    title_part = title or doi or "article"
    return sanitize_filename(f"{author_part}_{year_part}_{title_part}")


def provider_display_name(value: str) -> str:
    normalized = normalize_text(value).lower().replace("-", "_")
    from .provider_catalog import provider_display_names

    catalog_names = provider_display_names()
    if normalized in catalog_names:
        return catalog_names[normalized]
    return normalized.replace("_", " ").title() if normalized else "Provider"


def canonical_author_key(name: str) -> str:
    normalized = normalize_author_name(name)
    if not normalized:
        return ""
    if "," in normalized:
        parts = [part.strip() for part in normalized.split(",") if part.strip()]
        if len(parts) >= 2:
            normalized = " ".join(parts[1:] + [parts[0]])
    normalized = re.sub(r"[^a-z0-9]+", " ", normalized.lower())
    return re.sub(r"\s+", " ", normalized).strip()


def normalize_author_name(name: str) -> str:
    text = (name or "").replace("\xa0", " ")
    text = re.sub(r"[ \t\r\f\v]+", " ", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def dedupe_authors(authors: list[str]) -> list[str]:
    deduped: list[str] = []
    seen: set[str] = set()
    for author in authors:
        normalized_author = normalize_author_name(author)
        key = canonical_author_key(normalized_author)
        if not key or key in seen:
            continue
        seen.add(key)
        deduped.append(normalized_author)
    return deduped


def extension_from_content_type(
    content_type: str | None, source_url: str | None = None
) -> str:
    from .http.content_types import PDF_MIME_TYPE

    normalized = (content_type or "").split(";", 1)[0].strip().lower()
    known = {
        PDF_MIME_TYPE: ".pdf",
        "text/plain": ".txt",
        "text/xml": ".xml",
        "application/xml": ".xml",
        "application/jats+xml": ".xml",
        "application/json": ".json",
        "text/html": ".html",
        "image/jpeg": ".jpg",
        "image/png": ".png",
        "image/gif": ".gif",
        "image/webp": ".webp",
        "image/avif": ".avif",
        "image/tiff": ".tiff",
        "image/svg+xml": ".svg",
        "image/bmp": ".bmp",
        "image/x-ms-bmp": ".bmp",
        "image/vnd.microsoft.icon": ".ico",
        "image/x-icon": ".ico",
        "image/apng": ".apng",
        "image/heic": ".heic",
        "image/heif": ".heif",
    }
    if normalized in known:
        return known[normalized]
    if source_url:
        guessed = Path(urllib.parse.urlparse(source_url).path).suffix
        if guessed:
            return guessed
    return ".bin"


def build_output_path(
    output_dir: Path | None,
    doi: str | None,
    title: str | None,
    content_type: str | None,
    source_url: str | None,
    authors: list[str] | None = None,
    year: str | None = None,
) -> Path | None:
    if output_dir is None:
        return None
    output_dir.mkdir(parents=True, exist_ok=True)
    base_name = format_paper_stem(authors, year, title, doi=doi)
    extension = extension_from_content_type(content_type, source_url)
    return output_dir / f"{base_name}{extension}"


def save_payload(output_path: Path | None, body: bytes) -> str | None:
    if output_path is None:
        return None
    tmp_path = output_path.with_suffix(output_path.suffix + ".part")
    try:
        tmp_path.write_bytes(body)
        tmp_path.replace(output_path)
    except Exception:
        with contextlib.suppress(OSError):
            tmp_path.unlink(missing_ok=True)
        raise
    return str(output_path)


def empty_asset_results() -> dict[str, list[dict[str, Any]]]:
    return {
        "assets": [],
        "asset_failures": [],
    }


def build_asset_output_path(
    asset_dir: Path,
    source_href: str | None,
    content_type: str | None,
    source_url: str | None,
    used_names: set[str],
    *,
    preferred_filename: str | None = None,
) -> Path:
    candidate_name = ""
    for value in (preferred_filename, source_url, source_href):
        if not value:
            continue
        name = Path(urllib.parse.urlparse(value).path).name
        if name:
            candidate_name = name
            break

    stem = sanitize_filename(Path(candidate_name).stem or "asset")
    detected_suffix = extension_from_content_type(content_type)
    suffix = (
        detected_suffix
        if detected_suffix != ".bin"
        else (
            Path(candidate_name).suffix
            or extension_from_content_type(content_type, source_url or source_href)
        )
    )
    filename = f"{stem}{suffix}"

    counter = 2
    while filename in used_names or (asset_dir / filename).exists():
        filename = f"{stem}_{counter}{suffix}"
        counter += 1

    used_names.add(filename)
    return asset_dir / filename
