import logging
from datetime import datetime
from typing import NotRequired, TypedDict, cast
from xml.etree import ElementTree as ET

from defusedxml import ElementTree
from pydantic import BaseModel, ConfigDict

from labels.config.cache import dual_cache
from labels.enrichers.api_interface import make_get

LOGGER = logging.getLogger(__name__)


class MavenSearchRawDoc(TypedDict):
    id: str
    g: str
    a: str
    timestamp: int
    ec: list[str]
    v: NotRequired[str]
    latestVersion: NotRequired[str]
    p: NotRequired[str]
    tags: NotRequired[list[str]]


class MavenSearchDocResponse(BaseModel):
    id_: str
    group: str
    artifact: str
    version: str
    timestamp: int
    extra_classifiers: list[str]
    packaging: str | None = None
    tags: list[str] | None = None
    model_config = ConfigDict(frozen=True)


class MavenPackageInfo(BaseModel):
    group_id: str
    artifact_id: str
    latest_version: str | None = None
    release_date: int | None = None
    authors: list[str] | None = None
    version: str | None = None
    jar_url: str | None = None
    hash: str | None = None
    model_config = ConfigDict(frozen=True)


def build_maven_search_url(
    artifact_id: str,
    version: str | None,
) -> tuple[str, dict[str, str | int]]:
    base_url = "https://central.sonatype.com/solrsearch/select"
    query = f"a:{artifact_id} AND v:{version}" if version else f"a:{artifact_id}"
    params: dict[str, str | int] = {"q": query, "rows": 5, "wt": "json"}
    return base_url, params


def parse_maven_search_response(
    pkg_raw_data: MavenSearchRawDoc,
    artifact_id: str,
    version: str,
) -> MavenSearchDocResponse | None:
    try:
        return MavenSearchDocResponse(
            id_=pkg_raw_data["id"],
            group=pkg_raw_data["g"],
            artifact=pkg_raw_data["a"],
            version=pkg_raw_data["v"] if "v" in pkg_raw_data else pkg_raw_data["latestVersion"],
            packaging=pkg_raw_data.get("p"),
            timestamp=int(pkg_raw_data["timestamp"] // 1000),
            extra_classifiers=pkg_raw_data["ec"],
            tags=pkg_raw_data.get("tags"),
        )
    except KeyError as exc:
        LOGGER.exception(
            "Error parsing Maven search response",
            extra={
                "extra": {
                    "artifact_id": artifact_id,
                    "version": version,
                    "doc": pkg_raw_data,
                    "key": exc.args[0],  # type: ignore[misc]
                },
            },
        )
        return None


@dual_cache
def get_maven_search_response(artifact_id: str, version: str) -> MavenSearchRawDoc | None:
    base_url, params = build_maven_search_url(artifact_id, version)
    package_data = make_get(base_url, params=params, timeout=30)  # type: ignore[misc]
    if not package_data:  # type: ignore[misc]
        return None

    docs = package_data["response"].get("docs", [])  # type: ignore[misc]
    if not docs or len(docs) > 1:  # type: ignore[misc]
        return None
    return cast("MavenSearchRawDoc", docs[0])  # type: ignore[misc]


def search_maven_package(artifact_id: str, version: str) -> MavenSearchDocResponse | None:
    maven_raw_data = get_maven_search_response(artifact_id, version)
    if not maven_raw_data:
        return None
    return parse_maven_search_response(maven_raw_data, artifact_id, version)


def build_maven_urls(group_id: str, artifact_id: str, version: str) -> tuple[str, str, str]:
    group_id_path = group_id.replace(".", "/")
    base_path = f"https://repo1.maven.org/maven2/{group_id_path}/{artifact_id}/{version}"
    pom_url = f"{base_path}/{artifact_id}-{version}.pom"
    jar_url = f"{base_path}/{artifact_id}-{version}.jar"
    hash_url = f"{base_path}/{artifact_id}-{version}.jar.sha1"
    return pom_url, jar_url, hash_url


def get_package_hash(hash_url: str) -> str:
    hash_response: str | None = make_get(hash_url, content=True, timeout=30)
    return hash_response.strip() if hash_response else "Hash not available"


def parse_pom_xml(pom_content: str) -> ET.Element:
    return ElementTree.fromstring(pom_content)


def extract_authors(pom_xml: ET.Element) -> list[str]:
    namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
    return [
        author.text
        for author in pom_xml.findall("m:developers/m:developer/m:name", namespace)
        if author.text
    ]


@dual_cache
def get_authors(group_id: str, artifact_id: str, version: str) -> list[str] | None:
    pom_url, _, _ = build_maven_urls(group_id, artifact_id, version)
    response = make_get(pom_url, content=True, timeout=30)
    if not response:
        return None

    pom_xml = parse_pom_xml(response)
    return extract_authors(pom_xml)


def parse_metadata_xml(metadata_content: str) -> tuple[str | None, int | None]:
    metadata_xml = ElementTree.fromstring(metadata_content)
    latest_stable_version = metadata_xml.find("versioning/release")
    release_date_tag = metadata_xml.find("versioning/lastUpdated")

    release_date: int | None = None
    if release_date_tag is not None and release_date_tag.text:
        release_date = int(
            datetime.strptime(release_date_tag.text + "+0000", "%Y%m%d%H%M%S%z").timestamp(),
        )

    return (
        latest_stable_version.text if latest_stable_version is not None else None,
        release_date,
    )


def build_metadata_url(group_id: str, artifact_id: str) -> str:
    group_id_path = group_id.replace(".", "/")
    return f"https://repo1.maven.org/maven2/{group_id_path}/{artifact_id}/maven-metadata.xml"


@dual_cache
def get_maven_metadata(group_id: str, artifact_id: str) -> tuple[str | None, int | None]:
    metadata_url = build_metadata_url(group_id, artifact_id)
    response = make_get(metadata_url, content=True, timeout=30)
    if not response:
        return None, None

    return parse_metadata_xml(response)


@dual_cache
def get_package_hash_from_pom(
    group_id: str,
    artifact_id: str,
    version: str,
) -> tuple[str | None, str]:
    pom_url, jar_url, hash_url = build_maven_urls(group_id, artifact_id, version)
    response = make_get(pom_url, timeout=30, content=True)
    if not response:
        return None, jar_url

    return get_package_hash(hash_url), jar_url


def get_specific_version_info(
    group_id: str,
    artifact_id: str,
    version: str,
) -> MavenPackageInfo | None:
    package_hash, jar_url = get_package_hash_from_pom(group_id, artifact_id, version)

    if not package_hash:
        return None

    return MavenPackageInfo(
        group_id=group_id,
        artifact_id=artifact_id,
        version=version,
        jar_url=jar_url,
        hash=package_hash,
    )


def get_latest_version_info(group_id: str, artifact_id: str) -> MavenPackageInfo | None:
    latest_version, release_date = get_maven_metadata(group_id, artifact_id)
    if not latest_version:
        return None

    authors = get_authors(group_id, artifact_id, latest_version)

    return MavenPackageInfo(
        group_id=group_id,
        artifact_id=artifact_id,
        latest_version=latest_version,
        release_date=release_date,
        authors=authors if authors else None,
    )


def get_maven_package_info(
    group_id: str,
    artifact_id: str,
    version: str | None = None,
) -> MavenPackageInfo | None:
    if version:
        return get_specific_version_info(group_id, artifact_id, version)
    return get_latest_version_info(group_id, artifact_id)
