"""
Section-aware parser for project.md (FLY-681).

Parses and assembles project.md files using HTML comment markers to
delineate AI-generated content, workspace rules, and repo rules.

Pure stdlib module — no I/O, no third-party dependencies, Python 3.10+.
Imports one sibling, `status_vocab`, for the canonical status names.

Usage:
    from context_parser import parse_project_md, assemble_project_md, hash_sections

    result = parse_project_md(content)
    # result["sections"]["ai"], result["sections"]["workspace_rules"], result["sections"]["repo_rules"]
    # result["warnings"], result["ai_meta"]

    assembled = assemble_project_md(result["sections"], ai_meta=result["ai_meta"])
"""

import hashlib
import re
from typing import TypedDict

from status_vocab import CANONICAL_STATUSES

# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------

class ProjectMdSections(TypedDict):
    ai: str
    workspace_rules: str
    repo_rules: str


class ParseResult(TypedDict):
    sections: ProjectMdSections
    warnings: list[str]
    ai_meta: dict[str, str]


# ---------------------------------------------------------------------------
# Regex patterns — must match App contract exactly
# ---------------------------------------------------------------------------

AI_PATTERN = re.compile(
    r"<!--\s*flydocs:ai-start(?:\s+[^>]*)?\s*-->"
    r"([\s\S]*?)"
    r"<!--\s*flydocs:ai-end\s*-->"
)

WORKSPACE_PATTERN = re.compile(
    r"<!--\s*flydocs:workspace-rules-start(?:\s+[^>]*)?\s*-->"
    r"([\s\S]*?)"
    r"<!--\s*flydocs:workspace-rules-end\s*-->"
)

REPO_PATTERN = re.compile(
    r"<!--\s*flydocs:repo-rules-start(?:\s+[^>]*)?\s*-->"
    r"([\s\S]*?)"
    r"<!--\s*flydocs:repo-rules-end\s*-->"
)

# For detecting orphaned markers
AI_START = re.compile(r"<!--\s*flydocs:ai-start(?:\s+[^>]*)?\s*-->")
AI_END = re.compile(r"<!--\s*flydocs:ai-end\s*-->")
WS_START = re.compile(r"<!--\s*flydocs:workspace-rules-start(?:\s+[^>]*)?\s*-->")
WS_END = re.compile(r"<!--\s*flydocs:workspace-rules-end\s*-->")
REPO_START = re.compile(r"<!--\s*flydocs:repo-rules-start(?:\s+[^>]*)?\s*-->")
REPO_END = re.compile(r"<!--\s*flydocs:repo-rules-end\s*-->")

# Extract attributes from ai-start marker
AI_META_PATTERN = re.compile(
    r"<!--\s*flydocs:ai-start\s+(.*?)\s*-->"
)

ANY_MARKER = re.compile(r"<!--\s*flydocs:")


# ---------------------------------------------------------------------------
# Trim helper — strip exactly one leading and one trailing newline
# ---------------------------------------------------------------------------

def _trim_section(content: str) -> str:
    if content.startswith("\n"):
        content = content[1:]
    if content.endswith("\n"):
        content = content[:-1]
    return content


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def parse_project_md(content: str) -> ParseResult:
    """Parse a project.md file into sections by marker.

    Returns sections, warnings, and AI metadata. Content between markers
    is trimmed (one leading + one trailing newline stripped).

    If no markers are found, the entire content is treated as the AI section
    with a warning. Orphaned start/end markers produce a warning and an
    empty section.
    """
    warnings: list[str] = []
    ai_meta: dict[str, str] = {}

    # Check for any markers at all
    if not ANY_MARKER.search(content):
        return ParseResult(
            sections=ProjectMdSections(
                ai=content.strip(),
                workspace_rules="",
                repo_rules="",
            ),
            warnings=["No section markers found; treating entire file as AI content"],
            ai_meta={},
        )

    # Extract AI section
    ai_match = AI_PATTERN.search(content)
    if ai_match:
        ai_content = _trim_section(ai_match.group(1))
        # Extract meta attributes
        meta_match = AI_META_PATTERN.search(content)
        if meta_match:
            attrs = meta_match.group(1)
            for attr in re.finditer(r"(\w+)=(\S+)", attrs):
                ai_meta[attr.group(1)] = attr.group(2)
    else:
        ai_content = ""
        has_start = bool(AI_START.search(content))
        has_end = bool(AI_END.search(content))
        if has_start or has_end:
            warnings.append("Orphaned AI marker (start without end or vice versa); AI section treated as empty")

    # Extract workspace rules section
    ws_match = WORKSPACE_PATTERN.search(content)
    if ws_match:
        ws_content = _trim_section(ws_match.group(1))
    else:
        ws_content = ""
        has_start = bool(WS_START.search(content))
        has_end = bool(WS_END.search(content))
        if has_start or has_end:
            warnings.append("Orphaned workspace-rules marker; section treated as empty")

    # Extract repo rules section
    repo_match = REPO_PATTERN.search(content)
    if repo_match:
        repo_content = _trim_section(repo_match.group(1))
    else:
        repo_content = ""
        has_start = bool(REPO_START.search(content))
        has_end = bool(REPO_END.search(content))
        if has_start or has_end:
            warnings.append("Orphaned repo-rules marker; section treated as empty")

    return ParseResult(
        sections=ProjectMdSections(
            ai=ai_content,
            workspace_rules=ws_content,
            repo_rules=repo_content,
        ),
        warnings=warnings,
        ai_meta=ai_meta,
    )


def assemble_project_md(
    sections: ProjectMdSections,
    ai_meta: dict[str, str] | None = None,
) -> str:
    """Assemble sections into a marked-up project.md document.

    Canonical order: AI → workspace rules → repo rules, separated by
    blank lines. Empty sections are included with empty content between
    markers to preserve structure.
    """
    parts: list[str] = []

    # AI section
    ai_attrs = ""
    if ai_meta:
        attr_parts = [f"{k}={v}" for k, v in ai_meta.items()]
        if attr_parts:
            ai_attrs = " " + " ".join(attr_parts)
    parts.append(f"<!-- flydocs:ai-start{ai_attrs} -->")
    if sections["ai"]:
        parts.append(sections["ai"])
    parts.append("<!-- flydocs:ai-end -->")

    parts.append("")  # blank line separator

    # Workspace rules section
    parts.append("<!-- flydocs:workspace-rules-start synced-from=workspace -->")
    if sections["workspace_rules"]:
        parts.append(sections["workspace_rules"])
    parts.append("<!-- flydocs:workspace-rules-end -->")

    parts.append("")  # blank line separator

    # Repo rules section
    parts.append("<!-- flydocs:repo-rules-start -->")
    if sections["repo_rules"]:
        parts.append(sections["repo_rules"])
    parts.append("<!-- flydocs:repo-rules-end -->")

    return "\n".join(parts) + "\n"


def hash_sections(sections: ProjectMdSections) -> dict[str, str]:
    """Compute SHA-256 hashes for each section's content."""
    return {
        "ai": "sha256:" + hashlib.sha256(sections["ai"].encode("utf-8")).hexdigest(),
        "workspace_rules": "sha256:" + hashlib.sha256(
            sections["workspace_rules"].encode("utf-8")
        ).hexdigest(),
        "repo_rules": "sha256:" + hashlib.sha256(
            sections["repo_rules"].encode("utf-8")
        ).hexdigest(),
    }


def has_markers(content: str) -> bool:
    """Quick check for any flydocs section markers in content."""
    return bool(ANY_MARKER.search(content))


# ---------------------------------------------------------------------------
# Status workflow text (FLY-684)
# ---------------------------------------------------------------------------

# The rendered mapping walks CANONICAL_STATUSES (imported at the top) in
# display order. That list used to be retyped here; a status added to the
# vocabulary and not to the copy simply never appeared in the rendered
# provider mapping, and nothing said so (FLY-1272).
_WORKFLOW_HEADER = "## Status Workflow"
_WORKFLOW_FENCE = "<!-- flydocs:status-workflow -->"


def build_workflow_text(statuses_response: dict) -> str:
    """Build human-readable status workflow text from GET /api/statuses.

    Reference format from App's buildWorkflowRulesText:

        ## Status Workflow

        Provider: Jira | Last synced: 2026-04-11

        Mapping:
        - BACKLOG → Open (shared)
        - IMPLEMENTING → In Progress
        - COMPLETE → Done

    The statuses response shape:
        {
            "provider": "jira" | "linear",
            "states": [{"name": "Open", "id": "...", ...}, ...],
            "mapping": {"BACKLOG": "state-id", "READY": "state-id", ...}
        }
    """
    provider = statuses_response.get("provider", "unknown")
    states = statuses_response.get("states", [])
    mapping = statuses_response.get("mapping", {})

    # Build id → name lookup
    state_names: dict[str, str] = {}
    for s in states:
        sid = s.get("id") or s.get("_id") or ""
        state_names[sid] = s.get("name", sid)

    # Track which provider states map to multiple FlyDocs statuses (shared)
    provider_state_usage: dict[str, int] = {}
    for status in CANONICAL_STATUSES:
        target_id = mapping.get(status)
        if target_id:
            provider_state_usage[target_id] = provider_state_usage.get(target_id, 0) + 1

    from datetime import date
    today = date.today().isoformat()

    lines = [
        _WORKFLOW_FENCE,
        _WORKFLOW_HEADER,
        "",
        f"Provider: {provider.capitalize()} | Last synced: {today}",
        "",
        "Mapping:",
    ]

    for status in CANONICAL_STATUSES:
        target_id = mapping.get(status)
        if not target_id:
            continue
        target_name = state_names.get(target_id, target_id)
        shared = " (shared)" if provider_state_usage.get(target_id, 0) > 1 else ""
        lines.append(f"- {status} → {target_name}{shared}")

    # FLY-688: Instruct agents to use canonical names only
    lines.append("")
    lines.append(
        "Always use the left-hand canonical status names (e.g. IMPLEMENTING, "
        "REVIEW) for transitions. The relay translates to provider-native "
        "statuses automatically. Never use provider names directly."
    )
    # FLY-689: Document force escape hatch
    lines.append(
        "If a transition fails with STATUS_NOT_REACHABLE, retry with a "
        "different canonical status from the availableTransitions in the error "
        "response. As a last resort, use --force with a provider-native status "
        "name from the error response."
    )

    lines.append(_WORKFLOW_FENCE)
    return "\n".join(lines)


def strip_workflow_text(content: str) -> str:
    """Remove the status workflow block from section content.

    The workflow block is delimited by <!-- flydocs:status-workflow --> fences.
    Used before push diffing since workflow text is read-only (regenerated on pull).
    """
    pattern = re.compile(
        r"(?:\n\n|\n|^)"
        r"<!-- flydocs:status-workflow -->"
        r"[\s\S]*?"
        r"<!-- flydocs:status-workflow -->"
        r"(?:\n|$)"
    )
    return pattern.sub("", content).strip()


def inject_workflow_text(section_content: str, workflow_text: str) -> str:
    """Inject workflow text into a section, replacing any existing block.

    Strips existing workflow block first, then appends the new one.
    """
    cleaned = strip_workflow_text(section_content)
    if cleaned:
        return cleaned + "\n\n" + workflow_text
    return workflow_text
