"""Parse the breeder's free-text genotype notation into our frozen 8-locus
contract while losing nothing (FEAT-8b ruling from god):

  - mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re
  - rawGenotype  : the verbatim source string
  - unmappedTokens: tokens we couldn't map (Uw/Sls/Dea, markers like WFNZ/WP/DP, …)

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 '?'
    (frozen-contract wildcard; assumption pending the wife's confirmation)
"""
import re

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

# locus -> regex that matches that locus's token (longest alternatives first)
_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|-)?$"),
}
# loci our model does NOT have but the data uses
_KNOWN_UNMAPPED = re.compile(r"^(Uw|uw)(\[d\])?(Uw|uw)?(\[d\])?$|^(Sls|sls|Dea|dea)$", re.I)
_MARKER = re.compile(r"^\[?(WFNZ|WP|DP|GV|RV)\]?$|^\((taub|hörend|hoerend|RV|GV|extern[^)]*)\)$", re.I)


# one allele unit per locus (longest-match alternatives first); '-' = unknown
_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]|-"),
}


def _alleles_for(locus, token):
    """Extract the (allele1, allele2) pair from a single locus token, handling
    two-letter alleles (Sp/Re) and bracketed superscripts (c[chm] -> c^chm)."""
    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:
                # [-] = sub-allele unknown -> keep the base letter only
                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 free text/markers).

    Returns dict {mapped8locus, rawGenotype, unmappedTokens}.
    """
    raw = (raw or "").strip()
    mapped = {}
    unmapped = []
    # tokenise on whitespace; keep order
    for tok in raw.split():
        t = tok.strip().rstrip(",")
        if not t:
            continue
        matched = False
        for locus in LOCI:
            pat = _LOCUS_TOKEN.get(locus)
            if pat and pat.match(t):
                if locus not in mapped:   # first occurrence wins
                    mapped[locus] = _alleles_for(locus, t)
                matched = True
                break
        if matched:
            continue
        if _KNOWN_UNMAPPED.match(t) or _MARKER.match(t):
            unmapped.append(t)
        else:
            # anything else (stray notes, malformed tokens) -> unmapped, nothing lost
            unmapped.append(t)
    return {
        "mapped8locus": mapped,
        "rawGenotype": raw,
        "unmappedTokens": unmapped,
    }


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