"""
Core layer for Global Compiler logic.

Pure functions for formatting and assembling global compiler data.
These functions have no I/O and can be tested independently.

Reference: RFC-002 §4.4 Global Evolution
"""

from __future__ import annotations

import math
import re
from string import Formatter

import deal

from lattice.core.global_compiler_parse import (
    GlobalCandidatePattern,
    GlobalProposal,
    extract_cross_ref_content,
    extract_synthesis_content,
    extract_triage_content,
    parse_candidate_patterns,
    parse_global_proposals,
)


@deal.pre(
    lambda trace_content: isinstance(trace_content, str) and len(trace_content) > 0
)
@deal.post(lambda result: isinstance(result, int) and result >= 0)
def extract_sessions_count(trace_content: str) -> int:
    """Extract sessions count from trace content.

    Args:
        trace_content: The trace file content.

    Returns:
        Number of sessions processed, or 0 if not found.

    >>> extract_sessions_count("**Sessions**: 5")
    5
    >>> extract_sessions_count("No sessions here")
    0
    """
    match = re.search(r"\*\*Sessions\*\*:\s*(\d+)", trace_content)
    if match:
        return int(match.group(1))
    return 0


@deal.pre(
    lambda a, b: (
        isinstance(a, list)
        and isinstance(b, list)
        and len(a) > 0
        and len(b) > 0
        and len(a) == len(b)
        and all(isinstance(x, (int, float)) and math.isfinite(x) for x in a)
        and all(isinstance(x, (int, float)) and math.isfinite(x) for x in b)
        and any(x != 0 for x in a)
        and any(x != 0 for x in b)
    )
)
@deal.post(lambda result: -1.0001 <= result <= 1.0001)
def cosine_similarity(a: list[float], b: list[float]) -> float:
    """Compute cosine similarity between two vectors.

    Args:
        a: First vector.
        b: Second vector.

    Returns:
        Cosine similarity (-1 to 1).

    >>> cosine_similarity([1.0, 0.0], [1.0, 0.0])
    1.0
    >>> cosine_similarity([1.0, 0.0], [0.0, 1.0])
    0.0
    """
    dot_product = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(y * y for y in b))

    if norm_a == 0 or norm_b == 0:
        return 0.0

    result = dot_product / (norm_a * norm_b)

    # Handle floating point edge cases (NaN from inf/inf, etc.)
    if math.isnan(result) or math.isinf(result):
        return 0.0

    # Clamp to valid range [-1, 1] for numerical stability
    return max(-1.0, min(1.0, result))


@deal.pre(
    lambda trace_content, max_lines=50: (
        isinstance(trace_content, str) and isinstance(max_lines, int) and max_lines > 0
    )
)
@deal.post(lambda result: isinstance(result, str))
def extract_trace_preview(trace_content: str, max_lines: int = 50) -> str:
    """Extract a preview of trace content.

    Args:
        trace_content: Full trace content.
        max_lines: Maximum lines to include.

    Returns:
        Preview of trace content.

    >>> extract_trace_preview("line1\\nline2", max_lines=10)
    'line1\\nline2'
    >>> len(extract_trace_preview("a\\n" * 100, max_lines=10).split("\\n"))
    12
    """
    lines = trace_content.split("\n")
    if len(lines) <= max_lines:
        return trace_content

    return "\n".join(lines[:max_lines]) + "\n\n... (truncated)"


@deal.pre(
    lambda global_rules, project_rules_summaries, project_traces_summaries, embedding_clusters_summary, prompt_template: (
        isinstance(prompt_template, str)
        and len(prompt_template) > 0
        and _validate_template_placeholders(
            prompt_template,
            {"global_rules", "project_rules", "project_traces", "embedding_clusters"},
        )
    )
)
@deal.post(lambda result: isinstance(result, str) and len(result) > 0)
def assemble_global_phase1_prompt(
    global_rules: str,
    project_rules_summaries: list[str],
    project_traces_summaries: list[str],
    embedding_clusters_summary: str,
    prompt_template: str,
) -> str:
    """Assemble Phase 1 prompt for Global Compiler.

    Args:
        global_rules: Content of current global rules.
        project_rules_summaries: List of formatted project rules summaries.
        project_traces_summaries: List of formatted project traces summaries.
        embedding_clusters_summary: Formatted embedding clusters summary.
        prompt_template: Template with placeholders.

    Returns:
        Assembled prompt string.

    >>> prompt = assemble_global_phase1_prompt("rules", ["p1"], ["t1"], "clusters", "{global_rules}")
    >>> prompt
    'rules'
    """
    # Format project rules summary
    project_rules_str = "\n\n---\n\n".join(project_rules_summaries)

    # Format traces summary
    project_traces_str = (
        "\n\n---\n\n".join(project_traces_summaries)
        if project_traces_summaries
        else "(No project traces available)"
    )

    return prompt_template.format(
        global_rules=global_rules,
        project_rules=project_rules_str,
        project_traces=project_traces_str,
        embedding_clusters=embedding_clusters_summary,
    )


@deal.pre(
    lambda template, allowed: isinstance(template, str) and isinstance(allowed, set)
)
@deal.post(lambda result: isinstance(result, bool))
def _validate_template_placeholders(template: str, allowed: set[str]) -> bool:
    """Validate that template only contains allowed placeholders.

    Args:
        template: The template string to validate.
        allowed: Set of allowed placeholder names.

    Returns:
        True if all placeholders in template are in allowed set and template is valid.

    >>> _validate_template_placeholders("{foo}", {"foo"})
    True
    >>> _validate_template_placeholders("{bar}", {"foo"})
    False
    >>> _validate_template_placeholders("{foo} {baz}", {"foo", "baz"})
    True
    """
    formatter = Formatter()
    try:
        for _, field_name, _, _ in formatter.parse(template):
            if field_name is not None and field_name not in allowed:
                return False
        return True
    except ValueError:
        # Template has invalid format (e.g., unbalanced braces)
        return False


@deal.pre(
    lambda candidate_patterns, global_rules, evidence_summaries, prompt_template: (
        isinstance(candidate_patterns, list)
        and isinstance(global_rules, str)
        and isinstance(evidence_summaries, list)
        and isinstance(prompt_template, str)
        and len(prompt_template) > 0
        and _validate_template_placeholders(
            prompt_template, {"candidate_patterns", "global_rules", "project_evidence"}
        )
    )
)
@deal.post(lambda result: isinstance(result, str) and len(result) > 0)
def assemble_global_phase2_prompt(
    candidate_patterns: list[GlobalCandidatePattern],
    global_rules: str,
    evidence_summaries: list[str],
    prompt_template: str,
) -> str:
    """Assemble Phase 2 prompt for Global Compiler.

    Args:
        candidate_patterns: List of GlobalCandidatePattern from Phase 1.
        global_rules: Content of current global rules.
        evidence_summaries: List of formatted evidence summaries per candidate.
        prompt_template: Template with placeholders.

    Returns:
        Assembled prompt string.

    >>> from lattice.core.global_compiler_parse import GlobalCandidatePattern
    >>> candidate = GlobalCandidatePattern("Test", "desc", [], "HIGH", "rule", "test", 2)
    >>> prompt = assemble_global_phase2_prompt([candidate], "rules", ["evidence"], "{candidate_patterns}")
    >>> "CANDIDATE: Test" in prompt
    True
    """
    # Format candidate patterns
    patterns_str = ""
    for cp in candidate_patterns:
        patterns_str += (
            f"## CANDIDATE: {cp.pattern_name}\n"
            f"Pattern: {cp.description}\n"
            f"Source Projects: {', '.join(cp.source_projects)}\n"
            f"Confidence: {cp.confidence}\n"
            f"Evidence Type: {cp.evidence_type}\n"
            f"Rationale: {cp.rationale}\n"
            f"Convergence: {cp.convergence_count}\n\n"
        )

    # Format evidence summaries
    evidence_str = (
        "\n\n---\n\n".join(evidence_summaries)
        if evidence_summaries
        else "(No evidence retrieved)"
    )

    return prompt_template.format(
        candidate_patterns=patterns_str,
        global_rules=global_rules,
        project_evidence=evidence_str,
    )


@deal.pre(
    lambda phase1_cot, phase2_cot, projects_scanned: (
        isinstance(phase1_cot, str) and isinstance(phase2_cot, str)
    )
)
@deal.post(lambda result: isinstance(result, str) and len(result) > 0)
def format_global_trace_content(
    phase1_cot: str,
    phase2_cot: str,
    projects_scanned: int,
) -> str:
    """Format global evolution CoT phases into markdown trace content.

    Per RFC-002 §4.4: "The full CoT output (Phase 1 + Phase 2) is saved
    to ~/.lattice/drift/traces/ as an audit trail."

    Args:
        phase1_cot: Full CoT output from Phase 1 (rule scan).
        phase2_cot: Full CoT output from Phase 2 (evidence verification).
        projects_scanned: Number of projects scanned.

    Returns:
        Formatted markdown trace content.

    >>> trace = format_global_trace_content("phase1", "phase2", 5)
    >>> "## Phase 1: Rule Scan" in trace
    True
    >>> "## Phase 2: Evidence Verification" in trace
    True
    >>> "**Projects Scanned**: 5" in trace
    True
    """
    lines = [
        "# Global Evolution Trace",
        "",
        f"**Projects Scanned**: {projects_scanned}",
        "",
        "## Phase 1: Rule Scan",
        "",
        phase1_cot if phase1_cot else "(No output)",
        "",
        "## Phase 2: Evidence Verification",
        "",
        phase2_cot if phase2_cot else "(No output)",
        "",
    ]
    return "\n".join(lines)


@deal.pre(lambda proposals, timestamp: isinstance(proposals, list))
@deal.post(lambda result: isinstance(result, str) and len(result) > 0)
def format_global_proposal_content(
    proposals: list[GlobalProposal],
    timestamp: str,
) -> str:
    """Format global proposals into markdown content.

    Per RFC-002 §4.4: "Converged proposals are written to
    ~/.lattice/drift/proposals/ for review and application."

    Args:
        proposals: List of GlobalProposal objects.
        timestamp: ISO timestamp for the proposal.

    Returns:
        Formatted markdown proposal content.

    >>> from lattice.core.global_compiler_parse import GlobalProposal
    >>> prop = GlobalProposal(
    ...     title="Test",
    ...     action="ADD",
    ...     target="rules/test.md",
    ...     content="Test content",
    ...     source_projects=["p1", "p2"],
    ...     evidence_sessions=["s1"],
    ...     rationale="Test rationale",
    ... )
    >>> content = format_global_proposal_content([prop], "2026-02-19T00:00:00Z")
    >>> "## Proposal 1: Test" in content
    True
    >>> "**Converged from**: 2 projects" in content
    True
    """
    lines = [
        "# Global Evolution Proposals",
        "",
        f"**Generated**: {timestamp}",
        f"**Converged Proposals**: {len(proposals)}",
        "",
    ]

    if not proposals:
        lines.append("## No Converged Proposals")
        lines.append("")
        lines.append("No proposals reached convergence threshold (≥3 projects).")
        lines.append("")
        return "\n".join(lines)

    for i, p in enumerate(proposals, 1):
        lines.append(f"## Proposal {i}: {p.title}")
        lines.append("")
        lines.append(f"**Action**: {p.action}")
        lines.append(f"**Target**: {p.target}")
        lines.append(f"**Converged from**: {len(p.source_projects)} projects")
        lines.append("")
        lines.append("### Content")
        lines.append("")
        lines.append(p.content if p.content else "(No content)")
        lines.append("")
        lines.append("### Source Projects")
        lines.append("")
        lines.append(", ".join(p.source_projects) if p.source_projects else "None")
        lines.append("")
        lines.append("### Rationale")
        lines.append("")
        lines.append(p.rationale if p.rationale else "(No rationale)")
        lines.append("")

    return "\n".join(lines)


__all__ = [
    # Re-exported from parse module
    "GlobalCandidatePattern",
    "GlobalProposal",
    "parse_candidate_patterns",
    "parse_global_proposals",
    "extract_triage_content",
    "extract_cross_ref_content",
    "extract_synthesis_content",
    # Defined in this module
    "extract_sessions_count",
    "cosine_similarity",
    "extract_trace_preview",
    "assemble_global_phase1_prompt",
    "assemble_global_phase2_prompt",
    "format_global_trace_content",
    "format_global_proposal_content",
]
