"""Goal processor — sanitization, normalization, entity extraction.

Takes raw user input and produces a structured Goal with extracted
domain, entities, keywords, action verbs, complexity estimate, and
inferred constraints. Also handles multi-goal splitting when the
input contains multiple distinct objectives.
"""

import re
from typing import Any

from xyglue.goal.models import Goal, GoalContext, GoalConstraint


# ── Stop words to exclude from keyword extraction ──────────────────────
STOP_WORDS = {
    "that", "this", "with", "from", "have", "been", "will",
    "would", "could", "should", "them", "they", "their", "there",
    "what", "when", "where", "which", "while", "about", "after",
    "before", "between", "into", "through", "during", "each",
    "some", "other", "than", "then", "also", "just", "more",
    "very", "only", "need", "want", "make", "like", "does",
    "using", "used", "many", "much", "most", "such", "over",
    "even", "back", "still", "well", "take", "come", "these",
    "those", "being", "both", "same", "every", "under", "here",
}

# ── Action verbs that indicate what the user wants done ────────────────
ACTION_VERBS = {
    "build", "create", "make", "generate", "write", "develop",
    "implement", "design", "set up", "configure", "deploy",
    "scrape", "crawl", "extract", "download", "fetch", "pull",
    "send", "email", "notify", "message", "post", "publish",
    "analyze", "process", "transform", "convert", "parse",
    "monitor", "track", "watch", "check", "verify", "test",
    "update", "modify", "change", "edit", "fix", "patch",
    "delete", "remove", "clean", "purge", "reset",
    "search", "find", "query", "look up", "filter",
    "connect", "integrate", "link", "sync", "merge",
    "train", "classify", "predict", "cluster", "recommend",
    "schedule", "automate", "run", "execute", "trigger",
    "migrate", "backup", "restore", "export", "import",
}

# ── Complexity indicators ──────────────────────────────────────────────
HIGH_COMPLEXITY_SIGNALS = {
    "multiple", "several", "complex", "distributed", "microservice",
    "real-time", "realtime", "concurrent", "parallel", "pipeline",
    "machine learning", "deep learning", "neural", "kubernetes",
    "infrastructure", "architecture",
}
LOW_COMPLEXITY_SIGNALS = {
    "simple", "basic", "quick", "single", "one", "small", "tiny",
    "hello world", "example", "demo", "prototype", "test",
}

# ── Priority indicators ───────────────────────────────────────────────
PRIORITY_SIGNALS: dict[str, list[str]] = {
    "critical": ["urgent", "critical", "asap", "immediately", "emergency", "production down"],
    "high": ["important", "priority", "soon", "deadline", "needed"],
    "low": ["eventually", "nice to have", "low priority", "when you can", "someday"],
}


class GoalProcessor:
    """Processes raw goal input into a structured Goal.

    Pipeline stages:
    1. Sanitize — strip dangerous characters, normalize whitespace
    2. Normalize — capitalize, clean up formatting
    3. Extract context — domain, entities, keywords, actions
    4. Estimate complexity — based on word count and signal words
    5. Infer priority — based on urgency signals
    6. Detect constraints — budget, time, tool restrictions
    7. Split sub-goals — if the input contains multiple objectives
    """

    DOMAIN_KEYWORDS: dict[str, list[str]] = {
        "web": [
            "api", "rest", "endpoint", "server", "http", "https",
            "website", "frontend", "backend", "graphql", "webhook",
            "url", "html", "css", "javascript", "react", "django",
            "flask", "fastapi", "express", "nginx",
        ],
        "data": [
            "database", "sql", "data", "csv", "json", "xml",
            "analytics", "etl", "warehouse", "query", "table",
            "schema", "migration", "postgres", "mysql", "sqlite",
            "mongodb", "redis", "elasticsearch",
        ],
        "automation": [
            "scrape", "crawl", "automate", "bot", "schedule",
            "workflow", "pipeline", "cron", "selenium", "playwright",
            "puppeteer",
        ],
        "communication": [
            "email", "notify", "send", "message", "slack", "sms",
            "notification", "alert", "webhook", "chat",
        ],
        "ml": [
            "model", "train", "predict", "classify", "neural",
            "ml", "machine learning", "deep learning", "tensor",
            "pytorch", "sklearn", "regression", "clustering",
        ],
        "devops": [
            "deploy", "docker", "kubernetes", "ci/cd", "pipeline",
            "terraform", "ansible", "aws", "gcp", "azure", "cloud",
            "container", "infrastructure",
        ],
        "files": [
            "file", "directory", "folder", "upload", "download",
            "pdf", "image", "video", "audio", "document", "storage",
        ],
    }

    # ── Constraint detection patterns ──────────────────────────────────
    CONSTRAINT_PATTERNS: list[tuple[str, str, str]] = [
        # (regex, constraint_name, constraint_type)
        (r"budget\s*(?:of|:)?\s*\$?(\d+)", "budget_limit", "hard"),
        (r"under\s*\$(\d+)", "budget_limit", "hard"),
        (r"within\s+(\d+)\s*(minutes?|hours?|days?)", "time_limit", "hard"),
        (r"no\s+(?:more\s+than\s+)?(\d+)\s+api\s+calls?", "api_call_limit", "hard"),
        (r"must\s+use\s+(\w+)", "required_tool", "hard"),
        (r"don'?t\s+use\s+(\w+)", "excluded_tool", "hard"),
        (r"without\s+(\w+)", "excluded_tool", "soft"),
        (r"prefer\s+(\w+)", "preferred_tool", "preference"),
        (r"python\s+only", "language_constraint", "hard"),
        (r"no\s+external\s+(?:api|service)s?", "no_external_apis", "hard"),
    ]

    async def process(
        self, goal_input: str, context: dict[str, Any] | None = None
    ) -> Goal:
        """Full processing pipeline: sanitize, normalize, extract, enrich."""
        raw = goal_input.strip()
        normalized = self._normalize(raw)
        goal_context = self._extract_context(normalized)
        constraints = self._detect_constraints(normalized)
        sub_goals = self._split_sub_goals(normalized)

        merged_context = {**goal_context.to_dict(), **(context or {})}

        return Goal(
            raw=raw,
            normalized=normalized,
            context=merged_context,
            goal_context=goal_context,
            constraints=constraints,
            sub_goals=sub_goals,
        )

    # ── Sanitization ──────────────────────────────────────────────────

    def _sanitize(self, text: str) -> str:
        """Remove potentially dangerous characters and sequences."""
        # Strip HTML/XML-like tags
        text = re.sub(r"<[^>]+>", "", text)
        # Strip curly braces (template injection)
        text = re.sub(r"[{}]", "", text)
        # Normalize unicode quotes to ASCII
        text = text.replace("\u201c", '"').replace("\u201d", '"')
        text = text.replace("\u2018", "'").replace("\u2019", "'")
        # Collapse multiple spaces
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    # ── Normalization ─────────────────────────────────────────────────

    def _normalize(self, text: str) -> str:
        """Sanitize and normalize text to a standard form."""
        text = self._sanitize(text)
        text = re.sub(r"\s+", " ", text).strip()
        if text and not text[0].isupper():
            text = text[0].upper() + text[1:]
        # Remove trailing periods if there's only one sentence
        if text.endswith(".") and text.count(".") == 1:
            text = text[:-1].strip()
        return text

    # ── Context extraction ────────────────────────────────────────────

    def _extract_context(self, text: str) -> GoalContext:
        """Extract domain, entities, keywords, actions, complexity, priority."""
        text_lower = text.lower()

        domain = self._detect_domain(text_lower)
        keywords = self._extract_keywords(text_lower)
        entities = self._extract_entities(text)
        actions = self._extract_actions(text_lower)
        complexity = self._estimate_complexity(text_lower, keywords)
        priority = self._detect_priority(text_lower)

        return GoalContext(
            domain=domain,
            entities=entities,
            keywords=keywords[:20],
            actions=actions,
            complexity=complexity,
            priority=priority,
        )

    def _detect_domain(self, text_lower: str) -> str:
        """Identify the primary domain from text content."""
        scores: dict[str, int] = {}
        for domain, keywords in self.DOMAIN_KEYWORDS.items():
            score = sum(1 for k in keywords if k in text_lower)
            if score > 0:
                scores[domain] = score
        if not scores:
            return "general"
        return max(scores, key=scores.get)  # type: ignore[arg-type]

    def _extract_keywords(self, text_lower: str) -> list[str]:
        """Extract meaningful keywords, excluding stop words."""
        words = re.findall(r"\b\w{4,}\b", text_lower)
        seen: set[str] = set()
        result: list[str] = []
        for w in words:
            if w not in STOP_WORDS and w not in seen:
                seen.add(w)
                result.append(w)
        return result

    def _extract_entities(self, text: str) -> list[str]:
        """Extract named entities (capitalized multi-word sequences)."""
        # Match PascalCase or multi-word capitalized phrases
        entities = re.findall(r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b", text)
        # Also capture all-caps acronyms (3+ chars)
        acronyms = re.findall(r"\b[A-Z]{3,}\b", text)
        combined = entities + acronyms
        # Deduplicate preserving order
        seen: set[str] = set()
        result: list[str] = []
        for e in combined:
            if e not in seen and len(e) > 2:
                seen.add(e)
                result.append(e)
        return result

    def _extract_actions(self, text_lower: str) -> list[str]:
        """Identify action verbs present in the goal text."""
        found: list[str] = []
        for verb in ACTION_VERBS:
            if re.search(rf"\b{re.escape(verb)}\b", text_lower):
                found.append(verb)
        return found

    # ── Complexity estimation ─────────────────────────────────────────

    def _estimate_complexity(self, text_lower: str, keywords: list[str]) -> str:
        """Estimate goal complexity based on text signals."""
        for signal in LOW_COMPLEXITY_SIGNALS:
            if signal in text_lower:
                return "low"
        for signal in HIGH_COMPLEXITY_SIGNALS:
            if signal in text_lower:
                return "high"

        word_count = len(text_lower.split())
        keyword_count = len(keywords)

        if word_count <= 5 or keyword_count <= 2:
            return "low"
        if word_count >= 30 or keyword_count >= 10:
            return "high"
        return "medium"

    # ── Priority detection ────────────────────────────────────────────

    def _detect_priority(self, text_lower: str) -> str:
        """Detect urgency/priority from text signals."""
        for level, signals in PRIORITY_SIGNALS.items():
            for signal in signals:
                if signal in text_lower:
                    return level
        return "normal"

    # ── Constraint detection ──────────────────────────────────────────

    def _detect_constraints(self, text: str) -> list[GoalConstraint]:
        """Detect explicit constraints mentioned in the goal text."""
        constraints: list[GoalConstraint] = []
        text_lower = text.lower()
        for pattern, name, ctype in self.CONSTRAINT_PATTERNS:
            match = re.search(pattern, text_lower)
            if match:
                constraints.append(GoalConstraint(
                    name=name,
                    description=match.group(0),
                    constraint_type=ctype,
                    source="explicit",
                ))
        return constraints

    # ── Sub-goal splitting ────────────────────────────────────────────

    def _split_sub_goals(self, text: str) -> list[str]:
        """Split compound goals into sub-goals.

        Detects patterns like numbered lists, "and then", semicolons,
        or explicit multi-step instructions.
        """
        # Numbered list: "1. Do X  2. Do Y  3. Do Z"
        numbered = re.findall(r"\d+\.\s*(.+?)(?=\d+\.|$)", text)
        if len(numbered) >= 2:
            return [s.strip().rstrip(".") for s in numbered if s.strip()]

        # Semicolons: "Do X; then do Y; finally Z"
        if ";" in text:
            parts = [s.strip().rstrip(".") for s in text.split(";") if s.strip()]
            if len(parts) >= 2:
                return parts

        # "and then" / "then" / "after that" connectors
        split_pattern = r"\b(?:and\s+then|then|after\s+that|next|finally)\b"
        parts = re.split(split_pattern, text, flags=re.IGNORECASE)
        if len(parts) >= 2:
            return [s.strip().rstrip(".") for s in parts if len(s.strip()) > 5]

        return []
