from __future__ import annotations
import re
from typing import Dict, List

import tldextract

from domain_utils import normalize_domain, extract_etld1_and_subdomain, is_idn, to_unicode
from models import SyntaxInfo

try:
    import Levenshtein  # type: ignore
    HAVE_LEV = True
except ImportError:
    HAVE_LEV = False


SUSPICIOUS_TOKENS = {
    "login", "secure", "verify", "verification", "account", "support", "update",
    "billing", "invoice", "mail", "webmail", "signin", "password", "auth",
    "service", "client", "portal"
}


def damerau_levenshtein_py(a: str, b: str) -> int:
    da = {}
    maxdist = len(a) + len(b)
    d = [[0] * (len(b) + 2) for _ in range(len(a) + 2)]
    d[0][0] = maxdist
    for i in range(len(a) + 1):
        d[i + 1][0] = maxdist
        d[i + 1][1] = i
    for j in range(len(b) + 1):
        d[0][j + 1] = maxdist
        d[1][j + 1] = j

    for i in range(1, len(a) + 1):
        db = 0
        for j in range(1, len(b) + 1):
            i1 = da.get(b[j - 1], 0)
            j1 = db
            cost = 0 if a[i - 1] == b[j - 1] else 1
            if cost == 0:
                db = j
            d[i + 1][j + 1] = min(
                d[i][j] + cost,
                d[i + 1][j] + 1,
                d[i][j + 1] + 1,
                d[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1)
            )
        da[a[i - 1]] = i
    return d[len(a) + 1][len(b) + 1]


def distance(a: str, b: str) -> int:
    if HAVE_LEV:
        return Levenshtein.distance(a, b)  # type: ignore
    return damerau_levenshtein_py(a, b)


def tokenize(s: str) -> List[str]:
    return [t for t in re.split(r"[^a-z0-9]+", s.lower()) if t]


def char_script(ch: str) -> str:
    import unicodedata
    try:
        name = unicodedata.name(ch)
    except ValueError:
        return "UNKNOWN"
    for script in ("LATIN", "CYRILLIC", "GREEK", "ARABIC", "HEBREW", "HANGUL", "HIRAGANA", "KATAKANA", "CJK"):
        if script in name:
            return script
    if "DIGIT" in name:
        return "DIGIT"
    return "OTHER"


def detect_mixed_scripts(label: str) -> bool:
    scripts = set()
    for ch in label:
        if ch.isalnum():
            scripts.add(char_script(ch))
    scripts = {s for s in scripts if s not in ("DIGIT",)}
    return len(scripts) >= 2


def analyze_syntax(domain_input: str, brands: List[str]) -> SyntaxInfo:
    norm = normalize_domain(domain_input)
    etld1, sub, fqdn_provided = extract_etld1_and_subdomain(norm)
    etld1_u = to_unicode(etld1)

    ext = tldextract.extract(etld1)
    label = ext.domain or etld1.split(".")[0]
    label_u = to_unicode(label)

    idn_flag = is_idn(etld1) or is_idn(etld1_u)
    mixed = detect_mixed_scripts(label_u)

    tokens = set(tokenize(norm))
    suspicious = sorted(list(tokens.intersection(SUSPICIOUS_TOKENS)))

    brand_in_sub = []
    typo_matches: List[Dict[str, object]] = []

    if brands:
        # brand in subdomain (only meaningful if user passed a FQDN)
        if fqdn_provided and sub:
            sub_tokens = set(tokenize(sub))
            for b in brands:
                b_norm = normalize_domain(b)
                b_etld1, _, _ = extract_etld1_and_subdomain(b_norm)
                b_ext = tldextract.extract(b_etld1)
                b_label = b_ext.domain or b_etld1.split(".")[0]
                if b_label.lower() in sub_tokens:
                    brand_in_sub.append(b_label.lower())
            brand_in_sub = sorted(set(brand_in_sub))

        # typosquatting distance vs brand labels
        for b in brands:
            b_norm = normalize_domain(b)
            b_etld1, _, _ = extract_etld1_and_subdomain(b_norm)
            b_ext = tldextract.extract(b_etld1)
            b_label = (b_ext.domain or b_etld1.split(".")[0]).lower()
            d = distance(label.lower(), b_label)
            if 0 < d <= 2:
                typo_matches.append({"brand": b_etld1, "brand_label": b_label, "distance": d})
        typo_matches.sort(key=lambda x: (x["distance"], x["brand_label"]))

    return SyntaxInfo(
        input_domain=domain_input,
        fqdn_provided=fqdn_provided,
        registrable_domain=etld1_u,
        subdomain=sub,
        idn=idn_flag,
        mixed_scripts=mixed,
        suspicious_tokens=suspicious,
        brand_in_subdomain=brand_in_sub,
        typo_matches=typo_matches[:10],
    )
