"""
InsAIts Anchor Message Builder
================================
Loads templates from disk, varies whitespace/key ordering on each emission,
and builds layer-specific injection blocks that look like environment state.

Templates are loaded from insa_its/premium/anchor_templates/ first,
falling back to insa_its/anchor/templates/ if not found:
  layer1.txt  — Opus/orchestrator context block
  layer2.txt  — Agent session params
  layer3.txt  — Subagent constraints
  digest.txt  — CLAUDE.md digest fallback

Premium implementation — proprietary, not published to GitHub.
Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import hashlib
import logging
import os
import random
import re
from pathlib import Path
from typing import Dict, List, Optional

logger = logging.getLogger("insaits.anchor.builder")

# Forbidden words — never appear in injected content
_FORBIDDEN_WORDS = frozenset({
    "monitor", "detect", "security", "insaits",
    "anomaly", "warning", "flag",
})

# CLAUDE.md digest keywords to extract rules from
_DIGEST_KEYWORDS = [
    "plan", "verify", "fix", "skill", "elegance", "improve",
]

# Primary: premium/anchor_templates/ (ships with PyPI, gitignored from GitHub)
# Fallback: anchor/templates/ (public stubs on GitHub)
_PREMIUM_TEMPLATES_DIR = Path(__file__).parent / "anchor_templates"
_PUBLIC_TEMPLATES_DIR = Path(__file__).parent.parent / "anchor" / "templates"
_TEMPLATES_DIR = (
    _PREMIUM_TEMPLATES_DIR if _PREMIUM_TEMPLATES_DIR.is_dir()
    else _PUBLIC_TEMPLATES_DIR
)


class AnchorMessageBuilder:
    """Builds injection messages from templates with anti-pattern variation.

    Each call to build_layer*() returns slightly varied content:
    - Key ordering is shuffled in YAML-like blocks
    - Whitespace padding varies (1-3 trailing spaces on random lines)
    - Content hash changes on every emission → prevents pattern detection
    """

    def __init__(
        self,
        claude_md_path: Optional[str] = None,
        templates_dir: Optional[str] = None,
    ) -> None:
        self._claude_md_path = claude_md_path
        self._templates_dir = Path(templates_dir) if templates_dir else _TEMPLATES_DIR
        self._template_cache: Dict[str, tuple] = {}  # name -> (mtime, content)
        self._emission_count: int = 0

    def _load_template(self, name: str) -> str:
        """Load a template file with mtime-based caching."""
        path = self._templates_dir / name
        if not path.exists():
            logger.warning("Template not found: %s", path)
            return ""
        try:
            mtime = path.stat().st_mtime
        except OSError:
            return ""

        cached = self._template_cache.get(name)
        if cached and cached[0] == mtime:
            return cached[1]

        try:
            content = path.read_text(encoding="utf-8").strip()
            self._template_cache[name] = (mtime, content)
            return content
        except OSError as exc:
            logger.warning("Failed to read template %s: %s", name, exc)
            return ""

    def _vary_content(self, content: str) -> str:
        """Apply subtle variations to prevent identical emissions.

        Strategies:
        1. Shuffle key-value lines within YAML-like blocks
        2. Add random trailing whitespace to some lines
        3. Vary the block header spacing
        """
        self._emission_count += 1
        lines = content.split("\n")
        result: List[str] = []
        block_lines: List[str] = []

        for line in lines:
            # Detect block boundaries (lines starting with [ or empty)
            if line.startswith("[") or not line.strip():
                # Flush accumulated key-value lines with shuffle
                if block_lines:
                    random.shuffle(block_lines)
                    result.extend(block_lines)
                    block_lines = []
                # Add header/empty line with optional spacing variation
                if line.startswith("["):
                    result.append(line)
                else:
                    result.append(line)
            elif ":" in line and not line.startswith("#"):
                # Key-value line — accumulate for shuffling
                # Add random trailing space (1-3) on ~30% of lines
                if random.random() < 0.3:
                    line = line + " " * random.randint(1, 3)
                block_lines.append(line)
            else:
                # Comment or other line — pass through
                if block_lines:
                    random.shuffle(block_lines)
                    result.extend(block_lines)
                    block_lines = []
                result.append(line)

        # Flush remaining
        if block_lines:
            random.shuffle(block_lines)
            result.extend(block_lines)

        return "\n".join(result)

    def _validate_no_forbidden(self, content: str) -> str:
        """Ensure no forbidden words appear in content (case-insensitive).

        This is a safety check — templates should never contain them,
        but if the CLAUDE.md digest pulls them in, we strip them.
        """
        lower = content.lower()
        for word in _FORBIDDEN_WORDS:
            if word in lower:
                logger.warning(
                    "Forbidden word '%s' found in anchor content — removing", word
                )
                content = re.sub(
                    re.escape(word), "***", content, flags=re.IGNORECASE
                )
        return content

    def build_layer1(self) -> str:
        """Build Layer 1 (Opus/orchestrator) injection with CLAUDE.md digest.

        Always appends the digest block after the context block.
        """
        template = self._load_template("layer1.txt")
        digest = self._build_digest()
        if not template:
            return ""
        combined = template
        if digest:
            combined = template + "\n\n" + digest
        varied = self._vary_content(combined)
        return self._validate_no_forbidden(varied)

    def build_layer2(self) -> str:
        """Build Layer 2 (Agent) injection block."""
        template = self._load_template("layer2.txt")
        if not template:
            return ""
        varied = self._vary_content(template)
        return self._validate_no_forbidden(varied)

    def build_layer3(self) -> str:
        """Build Layer 3 (Subagent) injection block."""
        template = self._load_template("layer3.txt")
        if not template:
            return ""
        varied = self._vary_content(template)
        return self._validate_no_forbidden(varied)

    def build_for_layers(self, layers: List[int]) -> str:
        """Build combined injection for multiple layers.

        Layers are emitted in order: 1, 2, 3 — separated by blank lines.
        """
        builders = {
            1: self.build_layer1,
            2: self.build_layer2,
            3: self.build_layer3,
        }
        parts = []
        for layer in sorted(layers):
            builder = builders.get(layer)
            if builder:
                content = builder()
                if content:
                    parts.append(content)
        return "\n\n".join(parts)

    def _build_digest(self) -> str:
        """Build CLAUDE.md digest — extract critical rules from file.

        Reads the actual CLAUDE.md and extracts the 6 most operationally
        critical rules using keyword matching. Falls back to hardcoded
        digest template if file not found.
        """
        if self._claude_md_path:
            extracted = self._extract_from_claude_md(self._claude_md_path)
            if extracted:
                return extracted

        # Fallback: use digest.txt template
        return self._load_template("digest.txt")

    def _extract_from_claude_md(self, path: str) -> str:
        """Extract key rules from CLAUDE.md by keyword matching on headers."""
        try:
            full_path = Path(path)
            if not full_path.exists():
                return ""
            content = full_path.read_text(encoding="utf-8")
        except OSError:
            return ""

        rules: List[str] = []
        lines = content.split("\n")
        for i, line in enumerate(lines):
            lower = line.lower().strip()
            # Match headers containing our keywords
            if lower.startswith("#") or lower.startswith("**"):
                for kw in _DIGEST_KEYWORDS:
                    if kw in lower:
                        # Extract the rule: header + first content line
                        rule_text = line.strip().lstrip("#").lstrip("*").strip()
                        rules.append(rule_text)
                        break
            if len(rules) >= 6:
                break

        if not rules:
            return ""

        lines_out = ["[ACTIVE_RULES]"]
        for idx, rule in enumerate(rules, 1):
            # Clean up the rule text
            rule = rule.strip("*").strip()
            lines_out.append(f"{idx}. {rule}")
        return "\n".join(lines_out)

    def content_hash(self, content: str) -> str:
        """Generate a short hash of content for audit logging."""
        return hashlib.md5(content.encode()).hexdigest()[:8]
