"""Parse the breeder's free-text genotype notation into our locus model while
losing nothing (FEAT-8b + GEN-3b normalization, per hive/agents/god/GENETIK-notation.md):

  - mapped8locus  : {locus: [allele1, allele2]} for A C D E G P Sp Re (+ Sls when present)
  - rawGenotype   : the verbatim source string
  - unmappedTokens: tokens we still couldn't place
  - deaf          : True (dea/taub) | False (Dea/hörend) | None (not stated) — phenotype FLAG, not a locus
  - tags          : provenance/breeding markers (WFNZ/RV/GV/DP/extern …) — never genotype

GEN-3b normalizations (wife + research confirmed):
  - Uw/uw == G/g  (international vs German notation for the SAME locus) -> aliased to G/g.
  - Sls/WP        is a SECOND spotting locus (S(l)s(l) = WP/Minimalschecke het). WP -> Sls het.
  - Dea/dea/taub  -> hearing/deaf flag (written after spsp), NOT a Punnett locus.
  - WFNZ/RV/GV    -> provenance/breeding tags, NOT genotype, NOT conflict-bearing.

Conventions in the source data:
  - allele superscripts are bracketed: c[chm] -> c^chm, c[h] -> c^h, e[f] -> e^f
  - a single '-' for the second allele means "unknown"  -> mapped to '?'
"""
import re

LOCI = ["A", "C", "D", "E", "G", "P", "Sp", "Re"]

_LOCUS_TOKEN = {
    "Sp": re.compile(r"^(Sp|sp)(Sp|sp|-)?$"),
    "Re": re.compile(r"^(Re|re)(Re|re|-)?$"),
    "A":  re.compile(r"^(A|a)(A|a|-)?$"),
    "C":  re.compile(r"^(C|c)(\[(?:chm|chl|ch|h|hm|e|-)\])?(C|c|-)?(\[(?:chm|chl|ch|h|hm|e|-)\])?$"),
    "D":  re.compile(r"^(D|d)(D|d|-)?$"),
    "E":  re.compile(r"^(E|e)(\[(?:f|-)\])?(E|e|-)?(\[(?:f|-)\])?$"),
    "G":  re.compile(r"^(G|g)(G|g|-)?$"),
    "P":  re.compile(r"^(P|p)(P|p|-)?$"),
}

_ALLELE_UNIT = {
    "Sp": re.compile(r"Sp|sp|-"),
    "Re": re.compile(r"Re|re|-"),
    "C":  re.compile(r"[Cc]\[(?:chm|chl|ch|hm|h|e|-)\]|[Cc]|-"),
    "E":  re.compile(r"[Ee]\[(?:f|-)\]|[Ee]|-"),
    "A":  re.compile(r"[Aa]|-"),
    "D":  re.compile(r"[Dd]|-"),
    "G":  re.compile(r"[Gg]|-"),
    "P":  re.compile(r"[Pp]|-"),
}

# Provenance/breeding tags (never genotype): Wildfangnachzucht, Rückverpaarung,
# Geschwisterverpaarung, DarkPatch, external origin.
_TAG = re.compile(r"^\[?(WFNZ|RV|GV|DP)\]?$|^\((RV|GV|extern[^)]*)\)$", re.I)


def _rewrite_uw(token):
    """Uw/uw notation -> G/g (same locus). 'Uwuw[d]' -> 'Gg', 'UwUw' -> 'GG', 'uw[d]uw[d]' -> 'gg'."""
    if "uw" not in token.lower():
        return token
    return token.replace("uw[d]", "g").replace("Uw", "G").replace("uw", "g")


def _sls_alleles(token):
    """Sls (second spotting locus) alleles, or None. WP == Sls het (Minimalschecke);
    S(l)S(l) homozygous = lethal. Allele symbols: 'Sl' / 'sl'."""
    n = token.strip("[]").replace("(l)", "l").replace("(L)", "l")
    if n in ("WP", "Sls"):
        return ["Sl", "sl"]          # heterozygous (WP phenotype)
    if n.lower() == "sls":
        return ["sl", "sl"]          # wild-type (no extra spotting)
    units = re.findall(r"Sl|sl", n)
    return units if len(units) == 2 else None


def _deaf_value(token):
    """dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None. Case-sensitive for Dea/dea."""
    t = token.strip("()[]")
    if t == "dea" or t.lower() == "taub":
        return True
    if t == "Dea" or t.lower() in ("hörend", "hoerend"):
        return False
    return None


def _alleles_for(locus, token):
    pat = _ALLELE_UNIT.get(locus)
    units = pat.findall(token) if pat else re.findall(r"[A-Za-z](?:\[[a-z]+\])?|-", token)
    alleles = []
    for u in units:
        if u == "-":
            alleles.append("?")
        else:
            m = re.match(r"([A-Za-z]+)\[([a-z\-]+)\]", u)
            if m:
                alleles.append(m.group(1) if m.group(2) == "-" else f"{m.group(1)}^{m.group(2)}")
            else:
                alleles.append(u)
    if len(alleles) == 1:
        alleles.append("?")
    return alleles[:2]


def parse(raw):
    """raw: a genotype string (may include trailing markers/flags).

    Returns {mapped8locus, rawGenotype, unmappedTokens, deaf, tags}.
    """
    raw = (raw or "").strip()
    mapped = {}
    unmapped = []
    tags = []
    deaf = None

    for tok in raw.split():
        t = tok.strip().rstrip(",")
        if not t:
            continue

        # GEN-3b: Uw/uw is an alias of the G locus — rewrite before matching.
        t = _rewrite_uw(t)

        # 8 standard loci
        matched = False
        for locus in LOCI:
            pat = _LOCUS_TOKEN.get(locus)
            if pat and pat.match(t):
                mapped.setdefault(locus, _alleles_for(locus, t))   # first occurrence wins
                matched = True
                break
        if matched:
            continue

        # Sls (second spotting locus); WP is its heterozygous phenotype
        sls = _sls_alleles(t)
        if sls is not None:
            mapped.setdefault("Sls", sls)
            continue

        # deafness flag (after spsp): dea/taub vs Dea/hörend
        d = _deaf_value(t)
        if d is not None:
            deaf = d
            continue

        # provenance/breeding tags
        if _TAG.match(t):
            tags.append(re.sub(r"[()\[\]]", "", t).upper())
            continue

        unmapped.append(t)

    return {
        "mapped8locus": mapped,
        "rawGenotype": raw,
        "unmappedTokens": unmapped,
        "deaf": deaf,
        "tags": tags,
    }


def looks_like_genotype(text):
    """Heuristic: does this cell text contain >=3 recognisable locus tokens?"""
    n = 0
    for tok in text.split():
        t = _rewrite_uw(tok.rstrip(","))
        if any(p.match(t) for p in _LOCUS_TOKEN.values()):
            n += 1
    return n >= 3
