"""
Shell layer for Global Evolution orchestration.

This module implements the Global Compiler:
- Phase 1: Scan rules, traces from all projects, identify candidate patterns
- Phase 2: Verify candidates with evidence from project stores, generate proposals

Pure logic (parsing, formatting) is delegated to core/global_compiler.py.

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

# @invar:allow file_size: Global evolution module - single file for Phases 1 & 2 orchestration
from __future__ import annotations

import math
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

from returns.result import Failure, Result, Success

from lattice.core.global_compiler import (
    GlobalCandidatePattern,
    GlobalProposal,
    assemble_global_phase1_prompt,
    assemble_global_phase2_prompt,
    cosine_similarity,
    extract_cross_ref_content,
    extract_sessions_count,
    extract_synthesis_content,
    extract_trace_preview,
    extract_triage_content,
    parse_candidate_patterns,
    parse_global_proposals,
)
from lattice.core.types.config import CompilerConfig, EmbeddingConfig
from lattice.core.types.evidence import ProjectRegistration
from lattice.shell.projects import load_projects
from lattice.shell.config import resolve_global_dir


# @invar:allow shell_result: Path helper, delegates to resolve_global_dir which returns Result
# @invar:allow shell_pure_logic: Path computation, no I/O
def _get_global_path() -> Path:
    """Get the global lattice directory path (~/.config/lattice/)."""
    result = resolve_global_dir()
    if isinstance(result, Success):
        return result.unwrap()
    # Fallback (should not happen, but defensive)
    return Path.home() / ".config" / "lattice"


@dataclass(frozen=True)
class ProjectRules:
    """Rules collected from a single project.

    Attributes:
        project_name: Name of the project.
        project_path: Path to the project root.
        rules_content: Concatenated content of rules/*.md files.
        rule_files: List of rule file names.
        promoted_count: Number of rules with <!-- lattice:promoted --> marker.

    >>> rules = ProjectRules(
    ...     project_name="test",
    ...     project_path="/path/to/project",
    ...     rules_content="# Rule 1\\n\\nContent",
    ...     rule_files=["conventions.md"],
    ...     promoted_count=0,
    ... )
    >>> rules.project_name
    'test'
    """

    project_name: str
    project_path: str
    rules_content: str
    rule_files: list[str]
    promoted_count: int


@dataclass(frozen=True)
class ProjectTrace:
    """Most recent evolution trace from a project.

    Attributes:
        project_name: Name of the project.
        trace_content: Full content of the trace file.
        trace_timestamp: Timestamp of the trace file.
        sessions_processed: Number of sessions processed in this trace.

    >>> trace = ProjectTrace(
    ...     project_name="test",
    ...     trace_content="# Evolution Trace\\n...",
    ...     trace_timestamp="2026-02-17T10:00:00Z",
    ...     sessions_processed=5,
    ... )
    >>> trace.sessions_processed
    5
    """

    project_name: str
    trace_content: str
    trace_timestamp: str
    sessions_processed: int


@dataclass(frozen=True)
class EmbeddingCluster:
    """A cluster of similar content identified by embedding similarity.

    Attributes:
        cluster_id: Unique identifier for the cluster.
        projects: List of project names in this cluster.
        similarity_score: Average cosine similarity within cluster.
        content_preview: Preview of the clustered content.

    >>> cluster = EmbeddingCluster(
    ...     cluster_id="cluster-1",
    ...     projects=["proj-a", "proj-b"],
    ...     similarity_score=0.85,
    ...     content_preview="Type hints...",
    ... )
    >>> cluster.similarity_score
    0.85
    """

    cluster_id: str
    projects: list[str]
    similarity_score: float
    content_preview: str


@dataclass(frozen=True)
class GlobalPhase1Output:
    """Output from Global Compiler Phase 1.

    Attributes:
        candidates: List of CandidatePattern objects.
        triage_content: Raw triage phase content.
        cot_output: Full CoT output from LLM.

    >>> output = GlobalPhase1Output(
    ...     candidates=[],
    ...     triage_content="Triage output...",
    ...     cot_output="Full CoT...",
    ... )
    >>> len(output.candidates)
    0
    """

    candidates: list[GlobalCandidatePattern]
    triage_content: str
    cot_output: str


# @shell_complexity: File I/O per project (exist check, glob, read, promoted marker check)
def scan_project_rules(
    projects: list[ProjectRegistration],
) -> Result[list[ProjectRules], str]:
    """Scan rules/*.md from all registered projects.

    Excludes rules marked with <!-- lattice:promoted --> marker.
    Per RFC-002 §4.4: "Promoted rules are excluded from project instinct assembly."

    Args:
        projects: List of registered projects.

    Returns:
        Success(list of ProjectRules) for each project with rules.
        Failure(error_message) on I/O error.

    >>> from lattice.core.types.evidence import ProjectRegistration
    >>> projects = [ProjectRegistration(path="/nonexistent", name="test", registered_at="")]
    >>> result = scan_project_rules(projects)
    >>> isinstance(result, Success)
    True
    """
    all_rules: list[ProjectRules] = []

    for proj in projects:
        proj_path = Path(proj.path)
        rules_dir = proj_path / ".lattice" / "rules"

        if not rules_dir.exists():
            # Skip projects without rules
            all_rules.append(
                ProjectRules(
                    project_name=proj.name,
                    project_path=proj.path,
                    rules_content="(No rules directory)",
                    rule_files=[],
                    promoted_count=0,
                )
            )
            continue

        try:
            rule_files = sorted(rules_dir.glob("*.md"))
            contents: list[str] = []
            file_names: list[str] = []
            promoted_count = 0

            for rule_file in rule_files:
                content = rule_file.read_text(encoding="utf-8")

                # Check for promoted marker
                if "<!-- lattice:promoted -->" in content:
                    promoted_count += 1
                    # Skip promoted rules from assembly
                    continue

                contents.append(f"### {rule_file.name}\n\n{content}")
                file_names.append(rule_file.name)

            all_rules.append(
                ProjectRules(
                    project_name=proj.name,
                    project_path=proj.path,
                    rules_content="\n\n".join(contents) if contents else "(No rules)",
                    rule_files=file_names,
                    promoted_count=promoted_count,
                )
            )
        except OSError as e:
            return Failure(f"Failed to read rules for {proj.name}: {e}")

    return Success(all_rules)


# @shell_complexity: File I/O per project (exist check, sorted glob, stat, read)
def scan_project_traces(
    projects: list[ProjectRegistration],
) -> Result[list[ProjectTrace], str]:
    """Scan most recent drift/traces/ from each project.

    Per RFC-002 §4.4: "Phase 1 reads drift/traces/ rather than raw project logs.
    Traces are structured CoT outputs from project-level Compiler runs."

    Args:
        projects: List of registered projects.

    Returns:
        Success(list of ProjectTrace) with most recent trace per project.
        Failure(error_message) on I/O error.

    >>> from lattice.core.types.evidence import ProjectRegistration
    >>> projects = [ProjectRegistration(path="/nonexistent", name="test", registered_at="")]
    >>> result = scan_project_traces(projects)
    >>> isinstance(result, Success)
    True
    """
    all_traces: list[ProjectTrace] = []

    for proj in projects:
        proj_path = Path(proj.path)
        traces_dir = proj_path / "drift" / "traces"

        if not traces_dir.exists():
            # Skip projects without traces
            all_traces.append(
                ProjectTrace(
                    project_name=proj.name,
                    trace_content="(No traces directory)",
                    trace_timestamp="",
                    sessions_processed=0,
                )
            )
            continue

        try:
            trace_files = sorted(
                traces_dir.glob("*.md"),
                key=lambda f: f.stat().st_mtime,
                reverse=True,
            )

            if not trace_files:
                all_traces.append(
                    ProjectTrace(
                        project_name=proj.name,
                        trace_content="(No traces)",
                        trace_timestamp="",
                        sessions_processed=0,
                    )
                )
                continue

            # Read most recent trace
            recent_trace = trace_files[0]
            content = recent_trace.read_text(encoding="utf-8")
            timestamp = datetime.fromtimestamp(
                recent_trace.stat().st_mtime, tz=timezone.utc
            ).strftime("%Y-%m-%dT%H:%M:%SZ")

            # Extract sessions_processed from trace content (using Core function)
            sessions_processed = extract_sessions_count(content)

            all_traces.append(
                ProjectTrace(
                    project_name=proj.name,
                    trace_content=content,
                    trace_timestamp=timestamp,
                    sessions_processed=sessions_processed,
                )
            )
        except OSError as e:
            return Failure(f"Failed to read traces for {proj.name}: {e}")

    return Success(all_traces)


# @shell_complexity: Embedding API calls per project + pairwise similarity comparison
def compute_embedding_clusters(
    project_rules: list[ProjectRules],
    embedding_config: EmbeddingConfig | None,
    threshold: float = 0.8,
) -> Result[list[EmbeddingCluster], str]:
    """Optionally compute embedding clusters for similarity pre-screening.

    Per RFC-002 §4.4: "if embedding configured: similarity pre-screening
    (cosine > 0.8) to cluster candidates."

    Args:
        project_rules: List of ProjectRules to cluster.
        embedding_config: EmbeddingConfig or None.
        threshold: Cosine similarity threshold for clustering.

    Returns:
        Success(list of EmbeddingCluster) if embedding configured.
        Success([]) if no embedding config (skip pre-screening).
        Failure(error_message) on embedding error.
    """
    if embedding_config is None:
        return Success([])

    from lattice.shell.embeddings import embed_text

    # Embed each project's rules
    embeddings: list[tuple[str, list[float]]] = []

    for proj in project_rules:
        if proj.rules_content.startswith("(") and proj.rules_content.endswith(")"):
            # Skip empty/placeholder content
            continue

        emb_result = embed_text(embedding_config, proj.rules_content)
        if isinstance(emb_result, Failure):
            # Log warning but continue
            continue

        embeddings.append((proj.project_name, emb_result.unwrap()))

    if len(embeddings) < 2:
        return Success([])

    # Compute pairwise similarities and cluster (using Core function)
    clusters: list[EmbeddingCluster] = []
    cluster_id = 0

    for i, (name_i, emb_i) in enumerate(embeddings):
        for j, (name_j, emb_j) in enumerate(embeddings[i + 1 :], start=i + 1):
            similarity = cosine_similarity(emb_i, emb_j)

            if similarity >= threshold:
                clusters.append(
                    EmbeddingCluster(
                        cluster_id=f"cluster-{cluster_id}",
                        projects=[name_i, name_j],
                        similarity_score=similarity,
                        content_preview=f"Similar patterns in {name_i} and {name_j}",
                    )
                )
                cluster_id += 1

    return Success(clusters)


# @shell_complexity: File I/O with exist check, glob, read loop
def read_global_rules(global_path: Path) -> Result[str, str]:
    """Read global rules from ~/.lattice/rules/.

    Args:
        global_path: Path to global .lattice directory.

    Returns:
        Success(rules_content) or Failure(error_message).
    """
    rules_dir = global_path / "rules"
    if not rules_dir.exists():
        return Success("(No global rules)")

    try:
        rule_files = sorted(rules_dir.glob("*.md"))
        contents = []

        for rule_file in rule_files:
            content = rule_file.read_text(encoding="utf-8")
            contents.append(f"### {rule_file.name}\n\n{content}")

        return Success("\n\n".join(contents) if contents else "(No global rules)")
    except OSError as e:
        return Failure(f"Failed to read global rules: {e}")


# @shell_complexity: Full Phase 1 pipeline - coordinates 10+ Shell operations
def run_global_phase1(
    llm_config: CompilerConfig,
    embedding_config: EmbeddingConfig | None = None,
    prompt_template: str | None = None,
) -> Result[GlobalPhase1Output, str]:
    """Run Global Compiler Phase 1: Rule-Level Scan.

    This implements the full Phase 1 pipeline:
    1. Load all registered projects
    2. Scan rules/*.md from each project
    3. Scan most recent drift/traces/ from each project
    4. Optionally compute embedding clusters (if config available)
    5. Read current global rules
    6. Assemble Phase 1 prompt
    7. Call LLM
    8. Parse candidate patterns

    Per RFC-002 §4.4: "The Global Compiler uses the same Structured CoT
    framework as the Project Compiler, but operates in two phases."

    Args:
        llm_config: CompilerConfig for LLM client.
        embedding_config: Optional EmbeddingConfig for pre-screening.
        prompt_template: Path to Phase 1 prompt template file.

    Returns:
        Success(GlobalPhase1Output) with candidates.
        Failure(error_message) on any error.

    >>> # Without actual LLM, this would fail at LLM call
    >>> result = run_global_phase1(None, None, None)
    >>> isinstance(result, Failure)  # No LLM config
    True
    """
    from lattice.shell.llm import llm_complete

    # Step 1: Load registered projects
    projects_result = load_projects()
    if isinstance(projects_result, Failure):
        return projects_result

    projects = projects_result.unwrap()

    if not projects:
        return Failure("No registered projects found")

    # Step 2: Scan project rules
    rules_result = scan_project_rules(projects)
    if isinstance(rules_result, Failure):
        return rules_result

    project_rules = rules_result.unwrap()

    # Step 3: Scan project traces
    traces_result = scan_project_traces(projects)
    if isinstance(traces_result, Failure):
        return traces_result

    project_traces = traces_result.unwrap()

    # Step 4: Optional embedding pre-screening
    clusters_result = compute_embedding_clusters(project_rules, embedding_config)
    if isinstance(clusters_result, Failure):
        # Continue without clusters on embedding failure
        embedding_clusters = []
    else:
        embedding_clusters = clusters_result.unwrap()

    # Step 5: Read global rules
    global_path = _get_global_path()
    global_rules_result = read_global_rules(global_path)
    if isinstance(global_rules_result, Failure):
        return global_rules_result

    global_rules = global_rules_result.unwrap()

    # Step 6: Load prompt template
    if prompt_template is None:
        template_path = (
            Path(__file__).parent.parent.parent.parent
            / "data"
            / "global_compiler_prompt_phase1.md"
        )
        if not template_path.exists():
            return Failure(f"Phase 1 prompt template not found: {template_path}")
        try:
            prompt_template = template_path.read_text(encoding="utf-8")
        except OSError as e:
            return Failure(f"Failed to read prompt template: {e}")
    elif Path(prompt_template).exists():
        try:
            prompt_template = Path(prompt_template).read_text(encoding="utf-8")
        except OSError as e:
            return Failure(f"Failed to read prompt template: {e}")

    # Step 7: Format summaries for prompt assembly (using Core functions)
    # Format project rules summary
    rules_summaries = []
    for pr in project_rules:
        rules_summaries.append(
            f"### {pr.project_name}\n\n{pr.rules_content}\n\n"
            f"*Promoted rules: {pr.promoted_count}*"
        )

    # Format traces summary
    traces_summaries = []
    for pt in project_traces:
        if pt.trace_content.startswith("(") and pt.trace_content.endswith(")"):
            # Skip placeholder traces
            continue
        # Include only key sections from trace (using Core function)
        trace_preview = extract_trace_preview(pt.trace_content)
        traces_summaries.append(
            f"### {pt.project_name} (at {pt.trace_timestamp})\n\n{trace_preview}"
        )

    # Format embedding clusters
    if embedding_clusters:
        clusters_str = (
            "Pre-screened candidate clusters based on embedding similarity:\n\n"
        )
        for cluster in embedding_clusters:
            clusters_str += (
                f"- **{cluster.cluster_id}**: {', '.join(cluster.projects)} "
                f"(similarity: {cluster.similarity_score:.2f})\n"
                f"  Preview: {cluster.content_preview}\n\n"
            )
    else:
        clusters_str = "(No embedding pre-screening performed)"

    # Step 8: Assemble prompt (using Core function)
    prompt = assemble_global_phase1_prompt(
        global_rules=global_rules,
        project_rules_summaries=rules_summaries,
        project_traces_summaries=traces_summaries,
        embedding_clusters_summary=clusters_str,
        prompt_template=prompt_template,
    )

    # Step 9: Call LLM
    if llm_config is None:
        return Failure("LLM config is required for global evolution")

    llm_result = llm_complete(llm_config, prompt)
    if isinstance(llm_result, Failure):
        return llm_result

    cot_output = llm_result.unwrap()

    # Step 10: Parse candidate patterns (using Core functions)
    candidates = parse_candidate_patterns(cot_output)
    triage_content = extract_triage_content(cot_output)

    return Success(
        GlobalPhase1Output(
            candidates=candidates,
            triage_content=triage_content,
            cot_output=cot_output,
        )
    )


@dataclass(frozen=True)
class GlobalPhase2Output:
    """Output from Global Compiler Phase 2.

    Attributes:
        proposals: List of GlobalProposal objects.
        cross_ref_content: Raw cross_ref phase content.
        synthesis_content: Raw synthesis phase content.
        cot_output: Full CoT output from LLM.

    >>> output = GlobalPhase2Output(
    ...     proposals=[],
    ...     cross_ref_content="Cross-ref...",
    ...     synthesis_content="Synthesis...",
    ...     cot_output="Full CoT...",
    ... )
    >>> len(output.proposals)
    0
    """

    proposals: list[GlobalProposal]
    cross_ref_content: str
    synthesis_content: str
    cot_output: str


# @shell_complexity: Store I/O (connect, close), optional embedding, search, result formatting
def query_project_evidence(
    project_path: Path,
    query: str,
    embedding_config: EmbeddingConfig | None = None,
    limit: int = 5,
) -> Result[str, str]:
    """Query project store.db for evidence matching a candidate pattern.

    Uses hybrid_search: FTS5-only by default, or FTS5 + vector when embedding configured.

    Per RFC-002 §4.4: "For each candidate pattern from Phase 1, perform targeted
    queries against the relevant projects' store.db to retrieve supporting evidence."

    Args:
        project_path: Path to project root.
        query: Search query for evidence.
        embedding_config: Optional EmbeddingConfig for hybrid search.
        limit: Maximum results to return.

    Returns:
        Success(evidence_summary) or Failure(error_message).
    """
    from lattice.shell.hybrid_search import hybrid_search
    from lattice.shell.schema import create_store

    store_path = project_path / ".lattice" / "store.db"
    if not store_path.exists():
        return Success("(No project store)")

    try:
        conn = sqlite3.connect(f"file:{store_path}?mode=ro", uri=True)
    except sqlite3.Error as e:
        return Failure(f"Failed to open store: {e}")

    try:
        # Generate embedding if configured
        query_embedding = None
        if embedding_config is not None:
            from lattice.shell.embeddings import embed_text

            emb_result = embed_text(embedding_config, query)
            if isinstance(emb_result, Success):
                query_embedding = emb_result.unwrap()

        # Execute search
        search_result = hybrid_search(conn, query, query_embedding, limit)
        conn.close()

        if isinstance(search_result, Failure):
            return search_result

        results = search_result.unwrap()
        if not results:
            return Success("(No matching evidence)")

        # Format evidence summary
        evidence_lines = []
        for r in results[:limit]:
            session_id = getattr(r, "session_id", "unknown")
            content_preview = getattr(r, "content", "")[:200]
            evidence_lines.append(f"- Session {session_id}: {content_preview}...")

        return Success("\n".join(evidence_lines))
    except Exception as e:
        conn.close()
        return Failure(f"Evidence query failed: {e}")


# @shell_complexity: Evidence retrieval per candidate + LLM call + parsing
def run_global_phase2(
    candidates: list[GlobalCandidatePattern],
    llm_config: CompilerConfig,
    projects: list[ProjectRegistration],
    embedding_config: EmbeddingConfig | None = None,
    prompt_template: str | None = None,
) -> Result[GlobalPhase2Output, str]:
    """Run Global Compiler Phase 2: Evidence Verification.

    This implements the full Phase 2 pipeline:
    1. For each candidate, query relevant project stores for evidence
    2. Read current global rules
    3. Assemble Phase 2 prompt
    4. Call LLM
    5. Parse global proposals

    Per RFC-002 §4.4: "Phase 2 — Evidence Verification (targeted deep-dive)
    For each candidate pattern from Phase 1, perform targeted queries against
    the relevant projects' store.db to retrieve supporting evidence."

    Args:
        candidates: List of CandidatePattern from Phase 1.
        llm_config: CompilerConfig for LLM client.
        projects: List of registered projects.
        embedding_config: Optional EmbeddingConfig for hybrid search.
        prompt_template: Path to Phase 2 prompt template file.

    Returns:
        Success(GlobalPhase2Output) with proposals (may be empty).
        Failure(error_message) on actual errors.

    >>> result = run_global_phase2([], None, [], None, None)
    >>> isinstance(result, Success)  # Empty candidates is valid
    True
    >>> result.unwrap().proposals
    []
    """
    from lattice.shell.llm import llm_complete

    # Step 1: If no candidates, return empty success (valid upstream state)
    if not candidates:
        return Success(
            GlobalPhase2Output(
                proposals=[],
                cross_ref_content="",
                synthesis_content="",
                cot_output="No candidates to verify",
            )
        )

    # Step 2: Read global rules
    global_path = _get_global_path()
    global_rules_result = read_global_rules(global_path)
    if isinstance(global_rules_result, Failure):
        return global_rules_result

    global_rules = global_rules_result.unwrap()

    # Step 3: Query evidence for each candidate
    evidence_summaries: list[str] = []
    for candidate in candidates:
        summary_lines = [f"### Evidence for: {candidate.pattern_name}\n"]

        for proj_name in candidate.source_projects:
            # Find project path
            project_path = None
            for proj in projects:
                if proj.name == proj_name:
                    project_path = Path(proj.path)
                    break

            if project_path is None:
                continue

            # Query evidence
            evidence_result = query_project_evidence(
                project_path, candidate.description, embedding_config
            )
            if isinstance(evidence_result, Success):
                summary_lines.append(f"**{proj_name}**:\n{evidence_result.unwrap()}\n")

        evidence_summaries.append("\n".join(summary_lines))

    # Step 4: Load prompt template
    if prompt_template is None:
        template_path = (
            Path(__file__).parent.parent.parent.parent
            / "data"
            / "global_compiler_prompt_phase2.md"
        )
        if not template_path.exists():
            return Failure(f"Phase 2 prompt template not found: {template_path}")
        try:
            prompt_template = template_path.read_text(encoding="utf-8")
        except OSError as e:
            return Failure(f"Failed to read prompt template: {e}")
    elif Path(prompt_template).exists():
        try:
            prompt_template = Path(prompt_template).read_text(encoding="utf-8")
        except OSError as e:
            return Failure(f"Failed to read prompt template: {e}")

    # Step 5: Assemble prompt (using Core function)
    prompt = assemble_global_phase2_prompt(
        candidate_patterns=candidates,
        global_rules=global_rules,
        evidence_summaries=evidence_summaries,
        prompt_template=prompt_template,
    )

    # Step 6: Call LLM
    if llm_config is None:
        return Failure("LLM config is required for global evolution")

    llm_result = llm_complete(llm_config, prompt)
    if isinstance(llm_result, Failure):
        return llm_result

    cot_output = llm_result.unwrap()

    # Step 7: Parse proposals (using Core functions)
    proposals = parse_global_proposals(cot_output)
    cross_ref_content = extract_cross_ref_content(cot_output)
    synthesis_content = extract_synthesis_content(cot_output)

    return Success(
        GlobalPhase2Output(
            proposals=proposals,
            cross_ref_content=cross_ref_content,
            synthesis_content=synthesis_content,
            cot_output=cot_output,
        )
    )


# @shell_complexity: Path resolution with fallbacks + file I/O error handling
def mark_rule_promoted(project_path: Path, rule_file: str) -> Result[bool, str]:
    """Mark a rule file as promoted by adding lattice:promoted comment.

    Per RFC-002 §4.4: "Mark promoted rules in source projects with
    <!-- lattice:promoted --> to exclude from future scans."

    Args:
        project_path: Path to project root.
        rule_file: Relative path to rule file (e.g., "rules/style.md").

    Returns:
        Success(True) if marked, Failure(error) on error.

    >>> result = mark_rule_promoted(Path("/nonexistent"), "rules/test.md")
    >>> isinstance(result, Failure)
    True
    """
    # Resolve full path
    if rule_file.startswith("rules/"):
        full_path = project_path / ".lattice" / rule_file
    else:
        full_path = project_path / ".lattice" / "rules" / rule_file

    if not full_path.exists():
        # Try without .lattice prefix (direct rules/)
        alt_path = project_path / rule_file
        if alt_path.exists():
            full_path = alt_path
        else:
            return Failure(f"Rule file not found: {rule_file}")

    try:
        content = full_path.read_text(encoding="utf-8")
    except OSError as e:
        return Failure(f"Failed to read rule file: {e}")

    # Check if already marked
    if "<!-- lattice:promoted -->" in content:
        return Success(True)  # Already marked

    # Add marker at the top of file
    marked_content = f"<!-- lattice:promoted -->\n{content}"

    try:
        full_path.write_text(marked_content, encoding="utf-8")
    except OSError as e:
        return Failure(f"Failed to write rule file: {e}")

    return Success(True)


@dataclass(frozen=True)
class EvidenceRecord:
    """Evidence record for global.db.

    Attributes:
        source_project: Project name where evidence was found.
        source_session_id: Session ID from the source project.
        source_rule_file: Rule file if applicable.
        pattern: The global pattern name.
        summary: Evidence summary text.
        original_snippet: Original content snippet.
        confidence: Confidence score (0.0-1.0).

    >>> record = EvidenceRecord(
    ...     source_project="proj-a",
    ...     source_session_id="s1",
    ...     source_rule_file="rules/style.md",
    ...     pattern="Type Hints",
    ...     summary="Consistent use of type hints",
    ...     original_snippet="Always use type hints",
    ...     confidence=0.95,
    ... )
    >>> record.pattern
    'Type Hints'
    """

    source_project: str
    source_session_id: str
    source_rule_file: str | None
    pattern: str
    summary: str
    original_snippet: str | None
    confidence: float = 0.0


# @shell_complexity: Database connection + batch insert + error handling
def write_evidence_to_global_db(
    evidence_records: list[EvidenceRecord],
    global_db_path: Path | None = None,
) -> Result[list[int], str]:
    """Write evidence summaries to global.db evidence table.

    Per RFC-002 §5.3: "Insert evidence records with source tracking,
    pattern name, summary, and confidence score."

    Args:
        evidence_records: List of EvidenceRecord objects.
        global_db_path: Path to global.db (default: ~/.config/lattice/store/global.db).

    Returns:
        Success(list of rowids) for each inserted record.
        Failure(error) on database error.

    >>> result = write_evidence_to_global_db([], None)
    >>> isinstance(result, Success)
    True
    >>> result.unwrap()
    []
    """
    from datetime import datetime, timezone

    from lattice.shell.schema import create_global_store

    if global_db_path is None:
        global_db_path = _get_global_path() / "store" / "global.db"

    if not evidence_records:
        return Success([])

    # Open/create database
    store_result = create_global_store(global_db_path)
    if isinstance(store_result, Failure):
        return store_result

    conn = store_result.unwrap()

    try:
        extracted_at = datetime.now(timezone.utc).isoformat()

        cursor = conn.cursor()
        rowids: list[int] = []
        for record in evidence_records:
            cursor.execute(
                """
                INSERT INTO evidence (
                    source_project, source_session_id, source_rule_file,
                    pattern, summary, original_snippet, extracted_at, confidence
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.source_project,
                    record.source_session_id,
                    record.source_rule_file,
                    record.pattern,
                    record.summary,
                    record.original_snippet,
                    extracted_at,
                    record.confidence,
                ),
            )
            # cursor.lastrowid is guaranteed non-None after successful INSERT
            rowid = cursor.lastrowid
            if rowid is not None:
                rowids.append(rowid)

        conn.commit()
        return Success(rowids)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
    finally:
        conn.close()


# @shell_complexity: Database connection + batch association + error handling
def write_rule_evidence_associations(
    rule_file: str,
    evidence_ids: list[int],
    global_db_path: Path | None = None,
) -> Result[int, str]:
    """Write rule_evidence associations to global.db.

    Per RFC-002 §5.3: "Link global rules to their supporting evidence."

    Args:
        rule_file: Rule file name (e.g., "conventions.md").
        evidence_ids: List of evidence IDs to associate.
        global_db_path: Path to global.db (default: ~/.config/lattice/store/global.db).

    Returns:
        Success(count) with number of associations written.
        Failure(error) on database error.

    >>> result = write_rule_evidence_associations("conv.md", [], None)
    >>> isinstance(result, Success)
    True
    """
    from lattice.shell.schema import create_global_store

    if global_db_path is None:
        global_db_path = _get_global_path() / "store" / "global.db"

    if not evidence_ids:
        return Success(0)

    # Open/create database
    store_result = create_global_store(global_db_path)
    if isinstance(store_result, Failure):
        return store_result

    conn = store_result.unwrap()

    try:
        cursor = conn.cursor()
        for evidence_id in evidence_ids:
            cursor.execute(
                """
                INSERT OR IGNORE INTO rule_evidence (rule_file, evidence_id)
                VALUES (?, ?)
                """,
                (rule_file, str(evidence_id)),
            )

        conn.commit()
        return Success(len(evidence_ids))

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
    finally:
        conn.close()


# @shell_complexity: Database connection + embedding serialization + error handling
def write_evidence_embedding(
    evidence_id: int,
    embedding: list[float],
    global_db_path: Path | None = None,
) -> Result[bool, str]:
    """Write embedding to evidence_vec table.

    Per RFC-002 §5.3: "Store generated embeddings in evidence_vec for
    vector similarity search."

    Args:
        evidence_id: The evidence record ID.
        embedding: The embedding vector (768 dimensions).
        global_db_path: Path to global.db (default: ~/.config/lattice/store/global.db).

    Returns:
        Success(True) if written.
        Failure(error) on database error.
    """
    import struct

    from lattice.shell.schema import create_global_store

    if global_db_path is None:
        global_db_path = _get_global_path() / "store" / "global.db"

    # Validate embedding dimension
    if len(embedding) != 768:
        return Failure(f"Expected 768-dim embedding, got {len(embedding)}")

    # Open/create database
    store_result = create_global_store(global_db_path)
    if isinstance(store_result, Failure):
        return store_result

    conn = store_result.unwrap()

    try:
        # Convert embedding to blob (float32 array)
        embedding_blob = struct.pack(f"{len(embedding)}f", *embedding)

        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO evidence_vec (rowid, embedding) VALUES (?, ?)",
            (evidence_id, embedding_blob),
        )
        conn.commit()
        return Success(True)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
    finally:
        conn.close()


def write_global_evolution_output(
    phase1_cot: str,
    phase2_cot: str,
    proposals: list[GlobalProposal],
    projects_scanned: int,
    global_path: Path | None = None,
) -> Result[tuple[str, str], str]:
    """Write global evolution trace and proposals to drift/ directory.

    Creates two files:
    - ~/.lattice/drift/traces/<timestamp>.md — Full CoT trace (Phase 1 + Phase 2)
    - ~/.lattice/drift/proposals/<timestamp>.md — Converged proposals

    Per RFC-002 §4.4: "The full CoT output (Phase 1 + Phase 2) is saved to
    ~/.lattice/drift/traces/ as an audit trail. Converged proposals are
    written to ~/.lattice/drift/proposals/ for review and application."

    Args:
        phase1_cot: Full CoT output from Phase 1 (rule scan).
        phase2_cot: Full CoT output from Phase 2 (evidence verification).
        proposals: List of GlobalProposal objects (converged only).
        projects_scanned: Number of projects scanned.
        global_path: Path to global .lattice directory (default: ~/.lattice).

    Returns:
        Success((proposal_path, trace_path)) with absolute paths to files.
        Failure(error_message) on I/O error.

    >>> result = write_global_evolution_output("p1", "p2", [], 5, Path("/nonexistent_test_path"))
    >>> isinstance(result, Failure)  # Path doesn't exist
    True
    """
    from lattice.core.global_compiler import (
        format_global_proposal_content,
        format_global_trace_content,
    )

    if global_path is None:
        global_path = _get_global_path()

    try:
        # Ensure directories exist
        traces_dir = global_path / "drift" / "traces"
        proposals_dir = global_path / "drift" / "proposals"
        traces_dir.mkdir(parents=True, exist_ok=True)
        proposals_dir.mkdir(parents=True, exist_ok=True)

        # Generate timestamp for filenames
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
        iso_timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

        # Write trace file
        trace_filename = f"{timestamp}.md"
        trace_path = traces_dir / trace_filename
        trace_content = format_global_trace_content(
            phase1_cot, phase2_cot, projects_scanned
        )
        trace_path.write_text(trace_content, encoding="utf-8")

        # Write proposals file
        proposal_filename = f"{timestamp}.md"
        proposal_path = proposals_dir / proposal_filename
        proposal_content = format_global_proposal_content(proposals, iso_timestamp)
        proposal_path.write_text(proposal_content, encoding="utf-8")

        return Success((str(proposal_path), str(trace_path)))

    except OSError as e:
        return Failure(f"Failed to write global evolution output: {e}")


__all__ = [
    "ProjectRules",
    "ProjectTrace",
    "EmbeddingCluster",
    "GlobalPhase1Output",
    "GlobalPhase2Output",
    "EvidenceRecord",
    "scan_project_rules",
    "scan_project_traces",
    "compute_embedding_clusters",
    "read_global_rules",
    "run_global_phase1",
    "query_project_evidence",
    "run_global_phase2",
    "mark_rule_promoted",
    "write_evidence_to_global_db",
    "write_rule_evidence_associations",
    "write_evidence_embedding",
    "write_global_evolution_output",
]
