"""
Relationship cue scanner.

Scans intercepted text for explicit ISA / ISPART natural language patterns.
When matched, extracts a (subject, parent, dimension, is_isa) triple that
bypasses the saliency write threshold and goes directly to the write queue.

The `of {Z}` modifier after an ISA pattern names the dimension explicitly.
Without it, ISA defaults to dimension 'type'; ISPART defaults to 'membership'.

Noise filters applied to every extracted subject/parent:
  - Must be at least MIN_TOKEN_LEN characters
  - Must not be in the stopword list (pronouns, articles, common adj/adv/aux verbs)
  - Must not be a bare number
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Optional


# ---------------------------------------------------------------------------
# Noise filters
# ---------------------------------------------------------------------------

MIN_TOKEN_LEN = 3   # "it", "be", "do", "a", "an" all filtered by length alone

# Words that are structural glue in English and never meaningful KG concepts.
# Covers: pronouns, articles, demonstratives, common aux/modal verbs,
# common adjectives, common adverbs, prepositions, conjunctions.
_STOPWORDS: frozenset[str] = frozenset({
    # Personal pronouns
    "i", "me", "my", "myself", "we", "us", "our", "ours", "ourselves",
    "you", "your", "yours", "yourself", "yourselves",
    "he", "him", "his", "himself",
    "she", "her", "hers", "herself",
    "it", "its", "itself",
    "they", "them", "their", "theirs", "themselves",
    # Demonstrative / relative / interrogative pronouns
    "this", "that", "these", "those",
    "which", "who", "whom", "whose", "what", "where", "when", "how", "why",
    "whoever", "whatever", "whichever",
    # Articles
    "a", "an", "the",
    # Auxiliary / modal verbs
    "be", "been", "being", "am", "are", "was", "were",
    "have", "has", "had", "having",
    "do", "does", "did", "doing",
    "will", "would", "shall", "should",
    "may", "might", "must", "can", "could",
    "ought", "need", "dare", "used",
    # Common linking / existential
    "there", "here",
    # Prepositions / conjunctions / particles
    "in", "on", "at", "to", "for", "of", "with", "by", "from", "as",
    "into", "onto", "upon", "about", "above", "below", "between",
    "through", "during", "before", "after", "over", "under", "within",
    "without", "against", "along", "across", "behind", "beyond",
    "and", "or", "but", "nor", "so", "yet", "both", "either", "neither",
    "not", "no", "nor",
    "if", "then", "else", "although", "though", "while", "whereas",
    "because", "since", "unless", "until", "when", "than",
    # Common adjectives that are never useful KG concepts
    "new", "old", "good", "bad", "big", "small", "large", "little",
    "long", "short", "high", "low", "right", "left", "next", "last",
    "first", "second", "other", "same", "different", "few", "many",
    "much", "more", "most", "less", "least", "some", "any", "all",
    "every", "each", "both", "own", "such", "only", "just", "very",
    "too", "also", "again", "once", "now", "then", "still", "already",
    "always", "never", "often", "well", "back", "even", "way", "out",
    # Common verbs that appear as bare tokens
    "get", "got", "let", "put", "set", "go", "goes", "went",
    "make", "made", "take", "took", "come", "came", "know", "knew",
    "think", "thought", "see", "saw", "look", "use", "used",
    "want", "try", "ask", "work", "seem", "feel", "call", "keep",
    "give", "show", "run", "move", "live", "stand", "turn", "start",
    "play", "follow", "create", "include", "continue", "add", "become",
    # Boolean / value words
    "true", "false", "none", "null", "yes",
})


def _is_meaningful(token: str) -> bool:
    """
    Return True only if *token* could be a useful KG concept:
      - long enough
      - not a stopword
      - not a bare integer or float
    """
    if len(token) < MIN_TOKEN_LEN:
        return False
    if token in _STOPWORDS:
        return False
    try:
        float(token)
        return False        # bare number
    except ValueError:
        pass
    return True


@dataclass
class CueTriple:
    subject: str        # canonical token (lowercase, compound rule applied)
    parent: str         # canonical token
    dimension: str      # canonical token, defaults to 'type' or 'membership'
    is_isa: bool
    confidence: float   # pattern-type confidence


# ---------------------------------------------------------------------------
# Pattern definitions
# ---------------------------------------------------------------------------

# Each entry: (regex pattern, is_isa, base_confidence)
# Groups: (?P<subj>...) (?P<parent>...)  optional (?P<dim>...)
_RAW_PATTERNS: list[tuple[str, bool, float]] = [
    # ISA — explicit keyword forms (higher confidence)
    (r"(?P<subj>\S+)\s+ISA\s+(?P<parent>\S+)",                       True,  0.95),
    (r"(?P<subj>\S+)\s+is\s+an?\s+instance\s+of\s+(?P<parent>\S+)",  True,  0.90),
    (r"(?P<subj>\S+)\s+is\s+a\s+kind\s+of\s+(?P<parent>\S+)",        True,  0.90),
    (r"(?P<subj>\S+)\s+is\s+a\s+type\s+of\s+(?P<parent>\S+)",        True,  0.90),
    (r"(?P<subj>\S+)\s+kind\s+of\s+(?P<parent>\S+)",                  True,  0.80),
    (r"(?P<subj>\S+)\s+type\s+of\s+(?P<parent>\S+)",                  True,  0.80),
    (r"(?P<subj>\S+)\s+instance\s+of\s+(?P<parent>\S+)",              True,  0.80),
    # ISA — "is a / is an" (most common, must handle "of Z" modifier)
    (r"(?P<subj>\S+)\s+is\s+an?\s+(?P<parent>\S+)",                   True,  0.85),
    # ISPART — explicit keyword forms
    (r"(?P<subj>\S+)\s+ISPART\s+(?P<parent>\S+)",                     False, 0.95),
    (r"(?P<subj>\S+)\s+is\s+part\s+of\s+(?P<parent>\S+)",             False, 0.90),
    (r"(?P<subj>\S+)\s+is\s+a\s+member\s+of\s+(?P<parent>\S+)",       False, 0.90),
    (r"(?P<subj>\S+)\s+is\s+owned\s+by\s+(?P<parent>\S+)",            False, 0.90),
    (r"(?P<subj>\S+)\s+belongs\s+to\s+(?P<parent>\S+)",               False, 0.85),
    (r"(?P<subj>\S+)\s+part\s+of\s+(?P<parent>\S+)",                  False, 0.80),
    (r"(?P<subj>\S+)\s+member\s+of\s+(?P<parent>\S+)",                False, 0.80),
    (r"(?P<subj>\S+)\s+owned\s+by\s+(?P<parent>\S+)",                 False, 0.80),
    (r"(?P<subj>\S+)\s+runs\s+on\s+(?P<parent>\S+)",                  False, 0.85),
    (r"(?P<subj>\S+)\s+hosted\s+by\s+(?P<parent>\S+)",                False, 0.85),
    (r"(?P<subj>\S+)\s+deployed\s+on\s+(?P<parent>\S+)",              False, 0.85),
    (r"(?P<subj>\S+)\s+contained\s+in\s+(?P<parent>\S+)",             False, 0.85),
]

# Compiled patterns + metadata
_PATTERNS: list[tuple[re.Pattern, bool, float]] = [
    (re.compile(p, re.IGNORECASE), is_isa, conf)
    for p, is_isa, conf in _RAW_PATTERNS
]

# "of {Z}" dimension modifier following an ISA match (optional).
# Captures the first word only; _extend_compound() handles multi-word.
_OF_DIM_RE = re.compile(r"\s+of\s+(?P<dim>\S+)", re.IGNORECASE)

# Consecutive capital-starting words after a position
_CAPITAL_WORD_RE = re.compile(r"\s+([A-Z]\w*)")


# ---------------------------------------------------------------------------
# Token canonicalisation + compound extension
# ---------------------------------------------------------------------------

def _canonicalize(word: str) -> str:
    """Strip punctuation, apply lowercase. Single word — no compound rule."""
    return re.sub(r"^[^\w]+|[^\w]+$", "", word).lower()


def _extend_compound(text: str, after: int, first_word: str) -> str:
    """
    Starting from position `after` in `text`, consume any immediately-following
    capital-starting words and merge them with `first_word` into a single
    underscore-joined lowercase compound token.

    Example:
        text = "gnommoweb is part of Glitch University today"
        after = position after "Glitch"
        first_word = "Glitch"
        → returns "glitch_university"
    """
    words = [_canonicalize(first_word)]
    pos = after
    while True:
        m = _CAPITAL_WORD_RE.match(text, pos)
        if not m:
            break
        words.append(_canonicalize(m.group(1)))
        pos = m.end()
    return "_".join(words)


# ---------------------------------------------------------------------------
# Main scanner
# ---------------------------------------------------------------------------

def scan_cues(text: str) -> list[CueTriple]:
    """
    Scan *text* for relationship cue patterns. Returns all matched triples.
    Deduplicates by (subject, parent, dimension, is_isa).
    Multi-word proper-noun parents and dimensions are merged via _extend_compound().
    """
    results: list[CueTriple] = []
    seen: set[tuple] = set()

    for pattern, is_isa, base_conf in _PATTERNS:
        for m in pattern.finditer(text):
            raw_subj   = m.group("subj")
            raw_parent = m.group("parent")

            if not raw_subj or not raw_parent:
                continue

            subj   = _canonicalize(raw_subj)
            # Extend parent into compound if followed by more capital words
            parent = _extend_compound(text, m.end("parent"), raw_parent)

            if not _is_meaningful(subj) or not _is_meaningful(parent):
                continue

            # Check for "of {Z}" dimension modifier immediately after the match
            dimension: str
            if is_isa:
                suffix_start = m.end()
                of_match = _OF_DIM_RE.match(text, suffix_start)
                if of_match:
                    raw_dim = of_match.group("dim")
                    dimension = _extend_compound(text, of_match.end("dim"), raw_dim)
                else:
                    dimension = "type"
            else:
                dimension = _infer_ispart_dimension(m.re.pattern)

            # Reject if dimension was extracted but is a stopword (fall back to default)
            if dimension not in ("type", "membership", "runs-on", "owned-by", "geography", "tech"):
                if not _is_meaningful(dimension):
                    dimension = "type" if is_isa else "membership"

            key = (subj, parent, dimension, is_isa)
            if key not in seen:
                seen.add(key)
                results.append(CueTriple(
                    subject=subj,
                    parent=parent,
                    dimension=dimension,
                    is_isa=is_isa,
                    confidence=base_conf,
                ))

    return results


def _infer_ispart_dimension(pattern_str: str) -> str:
    """Guess a sensible default dimension from the ISPART pattern text."""
    if "runs" in pattern_str or "deployed" in pattern_str:
        return "runs-on"
    if "hosted" in pattern_str:
        return "runs-on"
    if "owned" in pattern_str:
        return "owned-by"
    if "part" in pattern_str or "contained" in pattern_str:
        return "membership"
    if "member" in pattern_str or "belongs" in pattern_str:
        return "membership"
    return "membership"
