"""Flow Gap Detector for SEF Agents.

Compares FLOWS.md definitions against REQ.md stories
to detect missing steps, orphan stories, and broken links.
"""

import re
from dataclasses import dataclass, field
from pathlib import Path

import structlog

from sef_agents.constants import Severity, Status
from sef_agents.tools.flow_mapper import scan_requirements_for_flows

logger = structlog.get_logger(__name__)

# Regex for parsing FLOWS.md step tables
# Format: | Step | Name | Description | Story ID | Status |
STEP_TABLE_ROW = re.compile(
    r"\|\s*(\d+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|"
)
FLOW_HEADER = re.compile(r"##\s*Flow:\s*(\S+)")
PROCESS_LINE = re.compile(r"\*\*Process:\*\*\s*(.+)")


@dataclass
class FlowDefinition:
    """Flow definition from FLOWS.md."""

    name: str
    process: str = ""
    steps: dict[str, dict[str, str]] = field(default_factory=dict)

    def add_step(
        self,
        order: int,
        name: str,
        description: str,
        story_id: str,
        status: str,
    ) -> None:
        """Add a step to the flow definition."""
        self.steps[name.strip()] = {
            "order": str(order),
            "name": name.strip(),
            "description": description.strip(),
            "story_id": story_id.strip(),
            "status": status.strip(),
        }


@dataclass
class Gap:
    """Represents a detected gap."""

    gap_type: str  # missing_story, orphan_story, broken_link, status_mismatch
    severity: str  # high, medium, low
    flow: str
    step: str
    description: str
    story_id: str = ""

    def to_dict(self) -> dict[str, str]:
        """Convert to dictionary."""
        return {
            "type": self.gap_type,
            "severity": self.severity,
            "flow": self.flow,
            "step": self.step,
            "description": self.description,
            "story_id": self.story_id,
        }


@dataclass
class GapReport:
    """Collection of detected gaps."""

    gaps: list[Gap] = field(default_factory=list)
    flows_checked: int = 0
    steps_checked: int = 0
    stories_checked: int = 0

    def add_gap(self, gap: Gap) -> None:
        """Add a gap to the report."""
        self.gaps.append(gap)

    @property
    def high_severity_count(self) -> int:
        """Count high severity gaps."""
        return sum(1 for g in self.gaps if g.severity == "high")

    @property
    def medium_severity_count(self) -> int:
        """Count medium severity gaps."""
        return sum(1 for g in self.gaps if g.severity == "medium")

    @property
    def low_severity_count(self) -> int:
        """Count low severity gaps."""
        return sum(1 for g in self.gaps if g.severity == "low")

    def to_markdown(self) -> str:
        """Generate markdown report."""
        lines = [
            "# Flow Gap Analysis Report",
            "",
            "*Auto-generated by flow_gap_detector.py*",
            "",
            "## Summary",
            "",
            f"- Flows checked: {self.flows_checked}",
            f"- Steps checked: {self.steps_checked}",
            f"- Stories checked: {self.stories_checked}",
            f"- **Total gaps: {len(self.gaps)}**",
            f"  - {Severity.HIGH_ICON} High: {self.high_severity_count}",
            f"  - {Severity.MEDIUM_ICON} Medium: {self.medium_severity_count}",
            f"  - {Severity.LOW_ICON} Low: {self.low_severity_count}",
            "",
        ]

        if not self.gaps:
            lines.append(
                f"{Status.SUCCESS} **No gaps detected. All flows are complete.**"
            )
            return "\n".join(lines)

        # Group by severity
        for severity, icon in [
            ("high", Severity.HIGH_ICON),
            ("medium", Severity.MEDIUM_ICON),
            ("low", Severity.LOW_ICON),
        ]:
            severity_gaps = [g for g in self.gaps if g.severity == severity]
            if severity_gaps:
                lines.append(f"## {icon} {severity.title()} Severity Gaps")
                lines.append("")
                lines.append("| Flow | Step | Type | Description |")
                lines.append("|------|------|------|-------------|")
                for gap in severity_gaps:
                    lines.append(
                        f"| {gap.flow} | {gap.step} | {gap.gap_type} | {gap.description} |"
                    )
                lines.append("")

        return "\n".join(lines)


def parse_flows_md(file_path: Path) -> dict[str, FlowDefinition]:
    """Parse FLOWS.md file to extract flow definitions.

    Args:
        file_path: Path to FLOWS.md file.

    Returns:
        Dict mapping flow name to FlowDefinition.
    """
    flows: dict[str, FlowDefinition] = {}

    try:
        content = file_path.read_text(encoding="utf-8")
    except OSError as e:
        logger.warning("flows_file_read_error", file=str(file_path), error=str(e))
        return flows

    current_flow: FlowDefinition | None = None
    lines = content.split("\n")

    for i, line in enumerate(lines):
        # Check for flow header
        flow_match = FLOW_HEADER.match(line)
        if flow_match:
            flow_name = flow_match.group(1)
            current_flow = FlowDefinition(name=flow_name)
            flows[flow_name] = current_flow
            continue

        # Check for process line
        if current_flow:
            process_match = PROCESS_LINE.match(line)
            if process_match:
                current_flow.process = process_match.group(1).strip()
                continue

        # Check for step table row
        if current_flow:
            step_match = STEP_TABLE_ROW.match(line)
            if step_match:
                order = int(step_match.group(1))
                name = step_match.group(2)
                description = step_match.group(3)
                story_id = step_match.group(4)
                status = step_match.group(5)

                # Skip header row
                if name.strip().lower() == "name":
                    continue

                current_flow.add_step(order, name, description, story_id, status)

    logger.info("flows_md_parsed", flows_found=len(flows))
    return flows


def detect_gaps(
    flows_file: str | Path,
    req_directory: str | Path,
) -> GapReport:
    """Detect gaps between FLOWS.md and REQ.md files.

    Args:
        flows_file: Path to FLOWS.md file.
        req_directory: Path to requirements directory.

    Returns:
        GapReport with all detected gaps.
    """
    report = GapReport()

    # Parse FLOWS.md
    flows_path = Path(flows_file)
    if not flows_path.exists():
        report.gaps.append(
            Gap(
                gap_type="config_error",
                severity="high",
                flow="-",
                step="-",
                description=f"FLOWS.md not found: {flows_file}",
            )
        )
        return report

    expected_flows = parse_flows_md(flows_path)
    report.flows_checked = len(expected_flows)

    # Scan REQ.md files for actual flow context
    actual_flow_map = scan_requirements_for_flows(req_directory)
    report.stories_checked = len(actual_flow_map.orphan_stories) + sum(
        len(f.steps) for f in actual_flow_map.flows.values()
    )

    # Check each expected flow
    for flow_name, expected_flow in expected_flows.items():
        actual_flow = actual_flow_map.flows.get(flow_name)
        report.steps_checked += len(expected_flow.steps)

        for step_name, expected_step in expected_flow.steps.items():
            story_id = expected_step["story_id"]

            # Gap 1: Missing story (step defined but no story linked)
            if story_id in ("-", "", "N/A", "Not Started"):
                report.add_gap(
                    Gap(
                        gap_type="missing_story",
                        severity="high",
                        flow=flow_name,
                        step=step_name,
                        description=f"Flow step '{step_name}' has no linked story",
                    )
                )
                continue

            # Gap 2: Story exists in FLOWS.md but not in REQ.md
            if actual_flow is None or step_name not in actual_flow.steps:
                report.add_gap(
                    Gap(
                        gap_type="missing_implementation",
                        severity="medium",
                        flow=flow_name,
                        step=step_name,
                        story_id=story_id,
                        description=f"Story {story_id} not found in requirements or missing flow context",
                    )
                )
                continue

            # Gap 3: Status mismatch
            actual_step = actual_flow.steps[step_name]
            expected_status = expected_step["status"].lower()
            actual_status = actual_step.status.lower()

            if expected_status != actual_status:
                report.add_gap(
                    Gap(
                        gap_type="status_mismatch",
                        severity="low",
                        flow=flow_name,
                        step=step_name,
                        story_id=story_id,
                        description=f"Status mismatch: FLOWS.md says '{expected_step['status']}', story says '{actual_step.status}'",
                    )
                )

    # Check for orphan flow steps (in REQ.md but not in FLOWS.md)
    for flow_name, actual_flow in actual_flow_map.flows.items():
        expected_flow = expected_flows.get(flow_name)

        if expected_flow is None:
            # Entire flow not in FLOWS.md
            for step_name, step in actual_flow.steps.items():
                report.add_gap(
                    Gap(
                        gap_type="orphan_flow",
                        severity="medium",
                        flow=flow_name,
                        step=step_name,
                        story_id=step.story_id,
                        description=f"Flow '{flow_name}' not defined in FLOWS.md",
                    )
                )
        else:
            # Check individual steps
            for step_name, step in actual_flow.steps.items():
                if step_name not in expected_flow.steps:
                    report.add_gap(
                        Gap(
                            gap_type="orphan_step",
                            severity="low",
                            flow=flow_name,
                            step=step_name,
                            story_id=step.story_id,
                            description=f"Step '{step_name}' in story but not in FLOWS.md",
                        )
                    )

    logger.info(
        "gap_detection_complete",
        gaps_found=len(report.gaps),
        high=report.high_severity_count,
        medium=report.medium_severity_count,
        low=report.low_severity_count,
    )

    return report


def detect_flow_gaps(flows_file: str, req_directory: str, summary: bool = False) -> str:
    """Detect and report flow gaps.

    Args:
        flows_file: Path to FLOWS.md file.
        req_directory: Path to requirements directory.
        summary: If True, return brief summary. If False, return full report.

    Returns:
        Markdown report or brief summary of detected gaps.
    """
    report = detect_gaps(flows_file, req_directory)

    # Summary mode
    if summary:
        if not report.gaps:
            return f"{Status.SUCCESS} No flow gaps detected."

        return (
            f"{Status.WARNING} {len(report.gaps)} gaps found: "
            f"{Severity.HIGH_ICON} {report.high_severity_count} high, "
            f"{Severity.MEDIUM_ICON} {report.medium_severity_count} medium, "
            f"{Severity.LOW_ICON} {report.low_severity_count} low"
        )

    # Full report mode
    markdown = report.to_markdown()

    # Write artifact
    output_path = Path(req_directory) / "FLOW_GAP_REPORT.md"
    try:
        output_path.write_text(markdown, encoding="utf-8")
        return f"Generated: `{output_path}`\n\n{markdown}"
    except OSError as e:
        return f"Failed to write artifact: {e}\n\n{markdown}"
