"""Typosquatting detector using Levenshtein distance against a large popular-package list."""
from __future__ import annotations

import re
from dataclasses import dataclass

from pyguard.models import Finding, Severity

# Top ~200 most-downloaded PyPI packages (snapshot; extended regularly)
POPULAR_PACKAGES: frozenset[str] = frozenset(
    {
        "requests",
        "numpy",
        "pandas",
        "scipy",
        "matplotlib",
        "scikit-learn",
        "tensorflow",
        "torch",
        "keras",
        "flask",
        "django",
        "fastapi",
        "starlette",
        "uvicorn",
        "gunicorn",
        "aiohttp",
        "httpx",
        "urllib3",
        "certifi",
        "charset-normalizer",
        "idna",
        "six",
        "packaging",
        "setuptools",
        "wheel",
        "pip",
        "virtualenv",
        "tox",
        "pytest",
        "pytest-cov",
        "pytest-asyncio",
        "coverage",
        "mypy",
        "ruff",
        "black",
        "isort",
        "flake8",
        "pylint",
        "bandit",
        "safety",
        "click",
        "typer",
        "rich",
        "colorama",
        "termcolor",
        "tqdm",
        "pydantic",
        "attrs",
        "cattrs",
        "marshmallow",
        "sqlalchemy",
        "alembic",
        "tortoise-orm",
        "peewee",
        "pymongo",
        "motor",
        "redis",
        "aioredis",
        "celery",
        "kombu",
        "boto3",
        "botocore",
        "google-cloud-storage",
        "google-auth",
        "azure-storage-blob",
        "azure-identity",
        "paramiko",
        "cryptography",
        "pyopenssl",
        "bcrypt",
        "passlib",
        "pyjwt",
        "oauthlib",
        "requests-oauthlib",
        "authlib",
        "pillow",
        "imageio",
        "opencv-python",
        "lxml",
        "beautifulsoup4",
        "html5lib",
        "scrapy",
        "selenium",
        "playwright",
        "pyarrow",
        "pyzmq",
        "websockets",
        "python-socketio",
        "grpcio",
        "protobuf",
        "openai",
        "anthropic",
        "langchain",
        "transformers",
        "tokenizers",
        "datasets",
        "huggingface-hub",
        "accelerate",
        "peft",
        "diffusers",
        "sentence-transformers",
        "nltk",
        "spacy",
        "gensim",
        "docker",
        "kubernetes",
        "ansible",
        "fabric",
        "invoke",
        "nox",
        "pre-commit",
        "commitizen",
        "bumpversion",
        "twine",
        "build",
        "hatchling",
        "flit",
        "poetry",
        "pdm",
        "pipenv",
        "pyyaml",
        "toml",
        "tomli",
        "dotenv",
        "python-dotenv",
        "loguru",
        "structlog",
        "sentry-sdk",
        "datadog",
        "prometheus-client",
        "opentelemetry-api",
        "arrow",
        "pendulum",
        "dateutil",
        "python-dateutil",
        "pytz",
        "tzdata",
        "schedule",
        "apscheduler",
        "celery",
        "dramatiq",
        "rq",
        "huey",
        "httpcore",
        "anyio",
        "trio",
        "asyncio",
        "twisted",
        "tornado",
        "gevent",
        "greenlet",
        "multiprocess",
        "joblib",
        "dask",
        "ray",
        "numba",
        "cython",
        "cffi",
        "ctypes",
        "swig",
        "pybind11",
        "mako",
        "jinja2",
        "markupsafe",
        "wtforms",
        "bleach",
        "tabulate",
        "xlrd",
        "openpyxl",
        "xlsxwriter",
        "reportlab",
        "fpdf",
        "pypdf",
        "pdfminer",
        "pytesseract",
        "psutil",
        "watchdog",
        "filelock",
        "tenacity",
        "backoff",
        "retry",
        "cachetools",
        "diskcache",
        "dogpile-cache",
        "hypothesis",
        "faker",
        "factory-boy",
        "responses",
        "httpretty",
        "respx",
        "moto",
        "freezegun",
        "time-machine",
        "pycparser",
        "pyparsing",
        "parsimonious",
        "lark",
        "ply",
        "graphql-core",
        "strawberry-graphql",
        "ariadne",
        "typer",
        "fire",
        "docopt",
        "argparse",
        "configparser",
        "dynaconf",
        "environ-config",
        "pyserial",
        "pyusb",
        "hid",
        "smbus",
    }
)


@dataclass(frozen=True, slots=True)
class TyposquatMatch:
    candidate: str
    target: str
    distance: int


def levenshtein_distance(left: str, right: str) -> int:
    if left == right:
        return 0
    if not left:
        return len(right)
    if not right:
        return len(left)

    previous = list(range(len(right) + 1))
    for i, left_char in enumerate(left, start=1):
        current = [i]
        for j, right_char in enumerate(right, start=1):
            insertions = previous[j] + 1
            deletions = current[j - 1] + 1
            substitutions = previous[j - 1] + (left_char != right_char)
            current.append(min(insertions, deletions, substitutions))
        previous = current
    return previous[-1]


def _normalise(name: str) -> str:
    """PEP 503 normalisation: lowercase + collapse hyphens/underscores/dots."""
    return re.sub(r"[-_.]+", "-", name).lower()


def find_typosquat_candidates(
    package_name: str,
    popular_packages: frozenset[str] | None = None,
) -> list[TyposquatMatch]:
    reference = popular_packages if popular_packages is not None else POPULAR_PACKAGES
    normalised = _normalise(package_name)
    matches: list[TyposquatMatch] = []
    for popular in reference:
        norm_popular = _normalise(popular)
        if normalised == norm_popular:
            return []  # exact match → not a typosquat
        distance = levenshtein_distance(normalised, norm_popular)
        if 0 < distance <= 2:
            matches.append(
                TyposquatMatch(candidate=package_name, target=popular, distance=distance)
            )
    return sorted(matches, key=lambda item: (item.distance, item.target))


def build_findings(package_name: str, version: str) -> list[Finding]:
    findings: list[Finding] = []
    for match in find_typosquat_candidates(package_name):
        severity = Severity.CRITICAL if match.distance == 1 else Severity.HIGH
        findings.append(
            Finding(
                severity=severity,
                title=f"Possible typosquat of '{match.target}'",
                description=(
                    f"Package name '{package_name}' is very close to well-known package "
                    f"'{match.target}' (edit distance: {match.distance}). "
                    "This may be a typosquatting attack."
                ),
                layer="static",
                package=package_name,
                version=version,
                evidence={"target": match.target, "distance": match.distance},
                confidence=0.85 if match.distance == 1 else 0.7,
                category="typosquatting",
                remediation=(
                    f"Did you mean '{match.target}'? "
                    "Verify the package name and intended source before installation."
                ),
            )
        )
    return findings
