"""Package metadata catalog — supply chain intelligence from Firestore.

Backed by the Firestore ``package_metadata`` collection (2,833 documents).
Used by CWE-1395 (supply chain/typosquatting) and CWE-1104 (vulnerable components)
detectors to make informed decisions with real package intelligence rather than
hardcoded popular-package lists.
"""
from __future__ import annotations

import difflib
import logging
import os
from dataclasses import dataclass, field
from functools import lru_cache
from typing import NamedTuple

logger = logging.getLogger(__name__)

_PROVIDER = os.environ.get("DSC_VULN_PROVIDER", "firestore")


@dataclass(frozen=True, slots=True)
class PackageMetadata:
	ecosystem: str              # "npm", "pypi", "maven"
	name: str
	latest_version: str = ""
	deprecated: bool = False
	maintainer_count: int = 0
	downloads_weekly: int = 0
	known_malicious: bool = False
	typosquat_targets: tuple[str, ...] = ()
	first_published: str = ""
	last_updated: str = ""
	metadata: dict = field(default_factory=dict)


class TyposquatMatch(NamedTuple):
	"""A typosquat target with an edit-distance confidence score."""
	target_name: str
	confidence: float  # 0.0–1.0 from difflib.SequenceMatcher


def _normalize_package_name(ecosystem: str, name: str) -> str:
	"""Normalize a package name following ecosystem conventions.

	- npm: lowercase only
	- PyPI: lowercase + hyphens replaced with underscores (PEP 503)
	"""
	lowered = name.lower()
	if ecosystem.lower() == "pypi":
		return lowered.replace("-", "_")
	return lowered


# ---------------------------------------------------------------------------
# In-memory store
# ---------------------------------------------------------------------------
_packages: list[PackageMetadata] = []
_loaded: bool = False


def _load_from_firestore() -> list[PackageMetadata]:
	from dsc.vulnerabilities.firestore_provider import fetch_package_metadata

	rows = fetch_package_metadata()
	if not rows:
		return []

	result: list[PackageMetadata] = []
	for row in rows:
		try:
			ecosystem = str(row.get("ecosystem", ""))
			raw_name = str(row.get("name", ""))
			normalized_name = _normalize_package_name(ecosystem, raw_name)
			raw_targets = row.get("typosquat_targets", [])
			normalized_targets = tuple(
				_normalize_package_name(ecosystem, str(t)) for t in raw_targets
			)
			result.append(PackageMetadata(
				ecosystem=ecosystem,
				name=normalized_name,
				latest_version=str(row.get("latest_version", "")),
				deprecated=bool(row.get("deprecated", False)),
				maintainer_count=int(row.get("maintainer_count", 0)),
				downloads_weekly=int(row.get("downloads_weekly", 0)),
				known_malicious=bool(row.get("known_malicious", False)),
				typosquat_targets=normalized_targets,
				first_published=str(row.get("first_published", "")),
				last_updated=str(row.get("last_updated", "")),
				metadata=dict(row.get("metadata", {})),
			))
		except Exception as exc:
			truncated = str(row)[:200]
			logger.warning("Skipping malformed package_metadata doc: %s — row: %s", exc, truncated)
	return result


def _ensure_loaded() -> None:
	global _packages, _loaded
	if _loaded:
		return
	if _PROVIDER == "firestore":
		loaded = _load_from_firestore()
		if loaded:
			_packages = loaded
			logger.info("DSC package metadata loaded from Firestore: %d records", len(loaded))
	_loaded = True
	packages_by_name.cache_clear()
	malicious_packages.cache_clear()
	deprecated_packages.cache_clear()


def reload() -> None:
	"""Force reload from Firestore (called by background refresh daemon)."""
	global _packages, _loaded
	if _PROVIDER != "firestore":
		return
	loaded = _load_from_firestore()
	if loaded:
		_packages = loaded
		packages_by_name.cache_clear()
		malicious_packages.cache_clear()
		deprecated_packages.cache_clear()
		logger.info("DSC package metadata reloaded: %d records", len(loaded))
	_loaded = True


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

@lru_cache(maxsize=1)
def packages_by_name() -> dict[str, PackageMetadata]:
	"""Lookup by (ecosystem:name) key, e.g. ``"npm:lodash"``."""
	_ensure_loaded()
	return {f"{p.ecosystem}:{p.name}".lower(): p for p in _packages}


@lru_cache(maxsize=1)
def malicious_packages() -> set[str]:
	"""Set of ``"ecosystem:name"`` keys for known-malicious packages."""
	_ensure_loaded()
	return {f"{p.ecosystem}:{p.name}".lower() for p in _packages if p.known_malicious}


@lru_cache(maxsize=1)
def deprecated_packages() -> set[str]:
	"""Set of ``"ecosystem:name"`` keys for deprecated packages."""
	_ensure_loaded()
	return {f"{p.ecosystem}:{p.name}".lower() for p in _packages if p.deprecated}


def get_package(ecosystem: str, name: str) -> PackageMetadata | None:
	"""Look up metadata for a specific package."""
	key = f"{ecosystem}:{name}".lower()
	return packages_by_name().get(key)


def is_known_malicious(ecosystem: str, name: str) -> bool:
	"""Check if a package is flagged as known-malicious."""
	return f"{ecosystem}:{name}".lower() in malicious_packages()


def is_deprecated(ecosystem: str, name: str) -> bool:
	"""Check if a package is deprecated."""
	return f"{ecosystem}:{name}".lower() in deprecated_packages()


def get_typosquat_targets(ecosystem: str, name: str) -> list[TyposquatMatch]:
	"""Return packages this name is a likely typosquat of, with confidence scores.

	Each result is a ``TyposquatMatch(target_name, confidence)`` where *confidence*
	is a 0.0–1.0 edit-distance similarity ratio between the queried *name* and the
	target, computed via ``difflib.SequenceMatcher``.  Results are sorted by
	descending confidence.
	"""
	pkg = get_package(ecosystem, name)
	if not pkg or not pkg.typosquat_targets:
		return []
	normalized = _normalize_package_name(ecosystem, name)
	matches = [
		TyposquatMatch(
			target_name=t,
			confidence=round(difflib.SequenceMatcher(None, normalized, t).ratio(), 4),
		)
		for t in pkg.typosquat_targets
	]
	matches.sort(key=lambda m: m.confidence, reverse=True)
	return matches
