"""
Static Analysis Engine for SystemVerilog / Verilog codebases.

Produces compiler-style structured reports ("design cards") with:
- label: what was detected (e.g. "fsm", "ready_valid_channel", "async_fifo")
- signals: which signals are involved
- evidence: line numbers + code snippets backing the claim
- confidence: 0.0–1.0 based on how many required conditions were met

This is EDA static analysis + pattern matching — no LLMs.
Everything is deterministic and audit-friendly.

Analysis passes (in order):
 1. always_block_classify  — ff / comb / latch classification
 2. clock_reset_detect     — clock/reset signals, polarity, sync/async
 3. fsm_detect             — state registers + case-based next-state logic
 4. interface_detect       — ready/valid, AXI, APB, AHB patterns
 5. driver_analysis        — single/multi-driver, undriven signals
 6. pipeline_detect        — valid shift chains, stage registers
 7. memory_detect          — register banks, address decode, memories
 8. fifo_detect            — sync/async FIFO patterns
 9. arbiter_detect         — fixed-priority, round-robin, weighted
10. cdc_detect             — signals crossing clock domains
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


# ============================================================================
# Data structures
# ============================================================================

@dataclass
class Finding:
    """A single analysis finding — one "design card"."""
    label: str                          # e.g. "fsm", "ready_valid_channel"
    category: str                       # e.g. "control_flow", "interface", "cdc"
    module: str                         # which module this was found in
    file: str                           # source file path
    confidence: float                   # 0.0–1.0
    signals: List[str] = field(default_factory=list)
    evidence: List[str] = field(default_factory=list)   # line snippets
    line_numbers: List[int] = field(default_factory=list)
    properties: Dict[str, Any] = field(default_factory=dict)  # detector-specific
    description: str = ""               # short structured description

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class ModuleContext:
    """Parsed context for a single module — input to all detectors."""
    name: str
    file: str
    raw_content: str                    # full module text (module...endmodule)
    start_line: int                     # line number in original file
    ports: Dict[str, Dict[str, Any]]    # name → {direction, width, type_str}
    params: Dict[str, str]              # name → default value
    always_blocks: List[Dict[str, Any]] # {type, sensitivity, body, line, raw}
    instances: List[Dict[str, str]]     # {module_type, instance_name, line}
    assigns: List[Dict[str, Any]]       # continuous assigns
    signals: Dict[str, Dict[str, Any]]  # internal signal declarations


# ============================================================================
# Module extraction — get structured module contexts from raw file content
# ============================================================================

def extract_module_contexts(content: str, file_path: str) -> List[ModuleContext]:
    """
    Extract all modules from a file as ModuleContext objects.

    This is the "front-end" — it parses the raw SV into structured blocks
    that the analysis passes can work on.
    """
    # Strip comments first
    stripped = _strip_comments(content)
    # Keep original content for line number mapping
    original_lines = content.split('\n')

    contexts: List[ModuleContext] = []

    # Find module...endmodule blocks
    # Use a state machine to handle nested generate/begin-end
    module_pattern = re.compile(r'\bmodule\s+(\w+)', re.MULTILINE)

    for m_match in module_pattern.finditer(stripped):
        module_name = m_match.group(1)
        mod_start = m_match.start()

        # Find matching endmodule
        endmod_pattern = re.compile(r'\bendmodule\b')
        endmod_match = endmod_pattern.search(stripped, m_match.end())
        if not endmod_match:
            continue

        mod_text = stripped[mod_start:endmod_match.end()]
        mod_raw = content[mod_start:endmod_match.end()]

        # Compute start line
        start_line = stripped[:mod_start].count('\n') + 1

        # Extract ports
        ports = _extract_ports_from_module(mod_text)

        # Extract parameters
        params = _extract_params_from_module(mod_text)

        # Extract always blocks
        always_blocks = _extract_always_blocks(mod_text, start_line)

        # Extract instances
        instances = _extract_instances_from_module(mod_text, start_line)

        # Extract continuous assigns
        assigns = _extract_assigns(mod_text, start_line)

        # Extract internal signal declarations
        signals = _extract_internal_signals(mod_text)

        contexts.append(ModuleContext(
            name=module_name,
            file=file_path,
            raw_content=mod_text,
            start_line=start_line,
            ports=ports,
            params=params,
            always_blocks=always_blocks,
            instances=instances,
            assigns=assigns,
            signals=signals,
        ))

    return contexts


# ============================================================================
# Analysis Pass 1: Always Block Classification
# ============================================================================

def classify_always_blocks(ctx: ModuleContext) -> List[Finding]:
    """
    Classify each always block as ff, comb, or latch.

    Rules:
    - always_ff → "flip_flop" (confidence 1.0)
    - always_comb → "combinational" (confidence 1.0)
    - always_latch → "latch" (confidence 1.0)
    - always @(posedge/negedge clk) → "flip_flop" (confidence 0.95)
    - always @(*) with full assignment → "combinational" (confidence 0.90)
    - always @(*) with incomplete assignment → "inferred_latch" (confidence 0.85)
    - always @(some_signal) → ambiguous (confidence 0.70)
    """
    findings = []

    for ab in ctx.always_blocks:
        ab_type = ab.get("type", "always")
        sensitivity = ab.get("sensitivity", "")
        body = ab.get("body", "")
        line = ab.get("line", 0)
        raw = ab.get("raw", "")[:200]

        if ab_type == "always_ff":
            findings.append(Finding(
                label="flip_flop",
                category="sequential_logic",
                module=ctx.name,
                file=ctx.file,
                confidence=1.0,
                signals=_extract_lhs_signals(body),
                evidence=[raw],
                line_numbers=[line],
                description=f"always_ff block — {len(_extract_lhs_signals(body))} registered signals",
            ))
        elif ab_type == "always_comb":
            findings.append(Finding(
                label="combinational",
                category="combinational_logic",
                module=ctx.name,
                file=ctx.file,
                confidence=1.0,
                signals=_extract_lhs_signals(body),
                evidence=[raw],
                line_numbers=[line],
                description=f"always_comb block — {len(_extract_lhs_signals(body))} driven signals",
            ))
        elif ab_type == "always_latch":
            findings.append(Finding(
                label="inferred_latch",
                category="latch",
                module=ctx.name,
                file=ctx.file,
                confidence=1.0,
                signals=_extract_lhs_signals(body),
                evidence=[raw],
                line_numbers=[line],
                description="always_latch block — intentional latch",
            ))
        else:
            # Plain "always" — classify by sensitivity list
            if re.search(r'posedge|negedge', sensitivity):
                # Edge-triggered → sequential
                clk_signals = re.findall(r'(?:posedge|negedge)\s+(\w+)', sensitivity)
                findings.append(Finding(
                    label="flip_flop",
                    category="sequential_logic",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=0.95,
                    signals=_extract_lhs_signals(body),
                    evidence=[raw],
                    line_numbers=[line],
                    properties={"clock_signals": clk_signals},
                    description=f"Edge-triggered always block on {', '.join(clk_signals)}",
                ))
            elif sensitivity.strip() in ('*', ''):
                # Level-sensitive — check for complete assignment
                lhs = _extract_lhs_signals(body)
                if_blocks = re.findall(r'\bif\b', body)
                case_blocks = re.findall(r'\bcase[xz]?\b', body)
                has_else = bool(re.search(r'\belse\b', body))
                has_default = bool(re.search(r'\bdefault\b', body))

                if (if_blocks and not has_else) or (case_blocks and not has_default):
                    findings.append(Finding(
                        label="inferred_latch",
                        category="latch",
                        module=ctx.name,
                        file=ctx.file,
                        confidence=0.85,
                        signals=lhs,
                        evidence=[raw],
                        line_numbers=[line],
                        description="Combinational block with incomplete assignment — likely inferred latch",
                    ))
                else:
                    findings.append(Finding(
                        label="combinational",
                        category="combinational_logic",
                        module=ctx.name,
                        file=ctx.file,
                        confidence=0.90,
                        signals=lhs,
                        evidence=[raw],
                        line_numbers=[line],
                        description="Level-sensitive always block — likely combinational",
                    ))

    return findings


# ============================================================================
# Analysis Pass 2: Clock / Reset Detection
# ============================================================================

def detect_clocks_resets(ctx: ModuleContext) -> List[Finding]:
    """
    Detect clock and reset signals with polarity and sync/async classification.

    Heuristics:
    - posedge/negedge in sensitivity lists → clock or async reset
    - Signals named *clk*, *clock* → clock (boost confidence)
    - Signals in if(!rst) or if(rst_n) pattern → reset
    - Reset inside sensitivity list → async reset
    - Reset only in body (not sensitivity) → sync reset
    """
    findings = []
    clock_signals: Dict[str, Dict[str, Any]] = {}
    reset_signals: Dict[str, Dict[str, Any]] = {}

    for ab in ctx.always_blocks:
        sensitivity = ab.get("sensitivity", "")
        body = ab.get("body", "")
        line = ab.get("line", 0)

        # Extract edge-triggered signals from sensitivity
        edge_signals = re.findall(r'(posedge|negedge)\s+(\w+)', sensitivity)

        for edge, sig in edge_signals:
            # Is it a clock or a reset?
            is_clock_name = bool(re.search(r'clk|clock|ck', sig, re.IGNORECASE))
            is_reset_name = bool(re.search(r'rst|reset|rstn|rst_n', sig, re.IGNORECASE))

            # Check if signal appears in reset conditional in the body
            reset_patterns = [
                rf'if\s*\(\s*!?\s*{re.escape(sig)}\s*\)',
                rf'if\s*\(\s*{re.escape(sig)}\s*==',
                rf'if\s*\(\s*~\s*{re.escape(sig)}\s*\)',
            ]
            appears_in_reset_cond = any(
                re.search(p, body) for p in reset_patterns
            )

            if appears_in_reset_cond or is_reset_name:
                polarity = "active_low" if (
                    re.search(rf'if\s*\(\s*!\s*{re.escape(sig)}', body) or
                    re.search(rf'if\s*\(\s*~\s*{re.escape(sig)}', body) or
                    re.search(rf'if\s*\(\s*{re.escape(sig)}\s*==\s*1\'b0', body) or
                    sig.lower().endswith(('_n', 'n')) and not sig.lower().endswith('en')
                ) else "active_high"

                reset_signals[sig] = {
                    "polarity": polarity,
                    "edge": edge,
                    "async": True,  # In sensitivity list = async
                    "line": line,
                }
            elif is_clock_name or not appears_in_reset_cond:
                # First edge signal that's not a reset is likely the clock
                if sig not in reset_signals:
                    clock_signals[sig] = {
                        "edge": edge,
                        "line": line,
                    }

        # Check for synchronous resets (in body only, not in sensitivity)
        sync_reset_pattern = re.compile(
            r'if\s*\(\s*(!?\s*\w*rst\w*|!?\s*\w*reset\w*)\s*\)',
            re.IGNORECASE,
        )
        for sr_match in sync_reset_pattern.finditer(body):
            sig_expr = sr_match.group(1).strip()
            sig_name = re.sub(r'^[!~]\s*', '', sig_expr)
            if sig_name not in reset_signals:
                is_negated = sig_expr.startswith(('!', '~'))
                reset_signals[sig_name] = {
                    "polarity": "active_low" if is_negated else "active_high",
                    "edge": "none",
                    "async": False,  # Not in sensitivity = sync
                    "line": line,
                }

    # Emit findings
    for sig, info in clock_signals.items():
        is_named_clk = bool(re.search(r'clk|clock|ck', sig, re.IGNORECASE))
        findings.append(Finding(
            label="clock_signal",
            category="clocking",
            module=ctx.name,
            file=ctx.file,
            confidence=0.95 if is_named_clk else 0.75,
            signals=[sig],
            line_numbers=[info["line"]],
            properties={"edge": info["edge"]},
            description=f"Clock signal '{sig}' ({info['edge']} edge)",
        ))

    for sig, info in reset_signals.items():
        async_str = "async" if info["async"] else "sync"
        findings.append(Finding(
            label=f"{async_str}_reset",
            category="reset",
            module=ctx.name,
            file=ctx.file,
            confidence=0.90,
            signals=[sig],
            line_numbers=[info["line"]],
            properties={
                "polarity": info["polarity"],
                "async": info["async"],
                "edge": info["edge"],
            },
            description=f"Reset signal '{sig}' — {info['polarity']}, {async_str}",
        ))

    return findings


# ============================================================================
# Analysis Pass 3: FSM Detection
# ============================================================================

def detect_fsms(ctx: ModuleContext) -> List[Finding]:
    """
    Detect Finite State Machines.

    Pattern: state register + case statement on state + next-state assignment.
    Also detects state enums and counts states.
    """
    findings = []

    # Look for state enums: typedef enum ... {STATE_A, STATE_B, ...} state_t;
    enum_pattern = re.compile(
        r'typedef\s+enum\s+(?:logic\s+)?(?:\[.*?\]\s+)?\{([^}]+)\}\s+(\w+)\s*;',
        re.DOTALL,
    )
    state_enums: Dict[str, List[str]] = {}
    for em in enum_pattern.finditer(ctx.raw_content):
        values = [v.strip().split('=')[0].strip() for v in em.group(1).split(',') if v.strip()]
        type_name = em.group(2)
        state_enums[type_name] = values

    # Look for state registers: <state_type> state, next_state;
    state_reg_pattern = re.compile(
        r'(\w+)\s+(state\w*|current_state|cs|present_state)\s*[,;]',
        re.IGNORECASE,
    )
    next_state_pattern = re.compile(
        r'(\w+)\s+(next_state\w*|ns|nxt_state)\s*[,;]',
        re.IGNORECASE,
    )

    state_regs = list(state_reg_pattern.finditer(ctx.raw_content))
    next_regs = list(next_state_pattern.finditer(ctx.raw_content))

    # Look for case statements on state-like variables
    for ab in ctx.always_blocks:
        body = ab.get("body", "")
        line = ab.get("line", 0)

        case_pattern = re.compile(
            r'case[xz]?\s*\(\s*(\w+)\s*\)',
            re.IGNORECASE,
        )
        for cm in case_pattern.finditer(body):
            case_var = cm.group(1)

            # Is this a case on a state variable?
            is_state_var = bool(re.search(
                r'state|cs|current_state|present_state',
                case_var,
                re.IGNORECASE,
            ))

            if not is_state_var:
                # Check if the case variable's type is an enum with STATE-like names
                for enum_type, values in state_enums.items():
                    state_like = sum(1 for v in values if re.match(
                        r'(S_|ST_|STATE_|IDLE|INIT|DONE|WAIT|READ|WRITE|FETCH|DECODE|EXEC)',
                        v, re.IGNORECASE,
                    ))
                    if state_like >= 2:
                        is_state_var = True
                        break

            if not is_state_var:
                continue

            # Count case branches (states)
            case_body = body[cm.start():]
            # Find the matching endcase
            endcase_match = re.search(r'\bendcase\b', case_body)
            if not endcase_match:
                continue

            case_block = case_body[:endcase_match.end()]
            branches = re.findall(r'^\s*(\w+)\s*:', case_block, re.MULTILINE)
            branches = [b for b in branches if b.lower() != 'default']

            # Check for next-state assignments
            has_next_assign = bool(re.search(
                r'(next_state|ns|nxt_state|state)\s*<?=',
                case_block,
                re.IGNORECASE,
            ))

            # Find which enum this might be
            state_names = branches
            for enum_type, values in state_enums.items():
                if set(branches) & set(values):
                    state_names = values
                    break

            conditions_met = sum([
                is_state_var,
                len(branches) >= 2,
                has_next_assign,
                bool(state_regs or next_regs),
            ])

            if conditions_met >= 2:
                findings.append(Finding(
                    label="fsm",
                    category="control_flow",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=min(0.5 + conditions_met * 0.15, 0.98),
                    signals=[case_var] + branches[:10],
                    evidence=[case_block[:300]],
                    line_numbers=[line],
                    properties={
                        "state_count": len(state_names),
                        "states": state_names[:20],
                        "has_next_state_reg": has_next_assign,
                        "case_variable": case_var,
                    },
                    description=(
                        f"FSM with {len(state_names)} states on '{case_var}'. "
                        f"States: {', '.join(state_names[:8])}"
                        f"{'...' if len(state_names) > 8 else ''}"
                    ),
                ))

    return findings


# ============================================================================
# Analysis Pass 4: Interface Pattern Detection
# ============================================================================

def detect_interfaces(ctx: ModuleContext) -> List[Finding]:
    """
    Detect common bus/handshake interface patterns by signal names.

    Detects:
    - Ready/Valid handshakes
    - AXI4 / AXI4-Lite interfaces
    - APB interfaces
    - AHB interfaces
    - Wishbone interfaces
    """
    findings = []
    port_names = set(ctx.ports.keys())
    all_names_lower = {p.lower(): p for p in port_names}

    # ── Ready/Valid channels ────────────────────────────────────────
    valid_signals = [p for p in port_names if re.search(r'valid|vld', p, re.IGNORECASE)]
    ready_signals = [p for p in port_names if re.search(r'ready|rdy', p, re.IGNORECASE)]

    for v_sig in valid_signals:
        # Try to find matching ready
        prefix = re.sub(r'(?i)(valid|vld).*', '', v_sig)
        matching_ready = [
            r for r in ready_signals
            if r.lower().startswith(prefix.lower()) or
            prefix.lower() in r.lower()
        ]
        if matching_ready:
            # Also look for data signals with same prefix
            data_signals = [
                p for p in port_names
                if p.lower().startswith(prefix.lower()) and
                p not in [v_sig] + matching_ready
            ]
            findings.append(Finding(
                label="ready_valid_channel",
                category="interface",
                module=ctx.name,
                file=ctx.file,
                confidence=0.92,
                signals=[v_sig] + matching_ready + data_signals[:5],
                properties={
                    "valid": v_sig,
                    "ready": matching_ready[0],
                    "prefix": prefix,
                    "data_signals": data_signals[:10],
                },
                description=f"Ready/Valid handshake: {v_sig} + {matching_ready[0]}",
            ))

    # ── AXI4 / AXI4-Lite ───────────────────────────────────────────
    axi_indicators = {
        "aw": ["awaddr", "awvalid", "awready"],
        "w": ["wdata", "wvalid", "wready"],
        "b": ["bresp", "bvalid", "bready"],
        "ar": ["araddr", "arvalid", "arready"],
        "r": ["rdata", "rvalid", "rready"],
    }

    for channel_name, required_sigs in axi_indicators.items():
        # Check each possible prefix (s_axi_, m_axi_, axi_, or just the signals)
        for prefix in ["s_axi_", "m_axi_", "axi_", ""]:
            found = sum(
                1 for s in required_sigs
                if f"{prefix}{s}" in all_names_lower
            )
            if found >= 2:
                all_channel = sum(
                    sum(1 for s in sigs if f"{prefix}{s}" in all_names_lower)
                    for sigs in axi_indicators.values()
                )
                total_possible = sum(len(sigs) for sigs in axi_indicators.values())

                # Check for AXI4 vs AXI4-Lite
                has_burst = f"{prefix}awlen" in all_names_lower or f"{prefix}arlen" in all_names_lower
                variant = "AXI4" if has_burst else "AXI4-Lite"

                # Check master vs slave
                aw_dir = ctx.ports.get(
                    all_names_lower.get(f"{prefix}awvalid", ""), {}
                ).get("direction", "")
                role = "master" if aw_dir == "output" else "slave" if aw_dir == "input" else "unknown"

                findings.append(Finding(
                    label="axi_interface",
                    category="interface",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=min(0.5 + (all_channel / total_possible) * 0.5, 0.98),
                    signals=[
                        all_names_lower[f"{prefix}{s}"]
                        for sigs in axi_indicators.values()
                        for s in sigs
                        if f"{prefix}{s}" in all_names_lower
                    ],
                    properties={
                        "variant": variant,
                        "role": role,
                        "prefix": prefix,
                        "channels_found": all_channel,
                        "channels_total": total_possible,
                    },
                    description=f"{variant} {role} interface (prefix='{prefix}', {all_channel}/{total_possible} signals)",
                ))
                break  # Found this prefix, don't check others

    # ── APB ─────────────────────────────────────────────────────────
    apb_sigs = ["psel", "penable", "pwrite", "paddr", "pwdata", "prdata", "pready"]
    apb_found = sum(1 for s in apb_sigs if any(s in p.lower() for p in port_names))
    if apb_found >= 4:
        findings.append(Finding(
            label="apb_interface",
            category="interface",
            module=ctx.name,
            file=ctx.file,
            confidence=min(0.5 + (apb_found / len(apb_sigs)) * 0.5, 0.98),
            signals=[p for p in port_names if any(s in p.lower() for s in apb_sigs)],
            description=f"APB interface ({apb_found}/{len(apb_sigs)} signals matched)",
        ))

    return findings


# ============================================================================
# Analysis Pass 5: Driver Analysis
# ============================================================================

def analyze_drivers(ctx: ModuleContext) -> List[Finding]:
    """
    Determine which signals are driven by which blocks.

    Flags:
    - Multi-driven signals (driven in multiple always blocks or assign + always)
    - Undriven outputs
    """
    findings = []

    # Collect all driven signals and their sources
    drivers: Dict[str, List[str]] = {}  # signal → [source1, source2, ...]

    for i, ab in enumerate(ctx.always_blocks):
        body = ab.get("body", "")
        lhs_signals = _extract_lhs_signals(body)
        for sig in lhs_signals:
            drivers.setdefault(sig, []).append(f"always_block_{i} (line {ab.get('line', '?')})")

    for i, assign in enumerate(ctx.assigns):
        lhs = assign.get("lhs", "")
        if lhs:
            drivers.setdefault(lhs, []).append(f"assign (line {assign.get('line', '?')})")

    # Check for multi-driven signals
    multi_driven = {sig: srcs for sig, srcs in drivers.items() if len(srcs) > 1}
    if multi_driven:
        for sig, srcs in multi_driven.items():
            findings.append(Finding(
                label="multi_driven",
                category="driver_analysis",
                module=ctx.name,
                file=ctx.file,
                confidence=0.95,
                signals=[sig],
                properties={"sources": srcs},
                description=f"Signal '{sig}' driven by {len(srcs)} sources: {', '.join(srcs)}",
            ))

    # Check for undriven outputs
    for port_name, port_info in ctx.ports.items():
        if port_info.get("direction") == "output":
            if port_name not in drivers:
                findings.append(Finding(
                    label="undriven_output",
                    category="driver_analysis",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=0.80,
                    signals=[port_name],
                    description=f"Output port '{port_name}' has no detected driver",
                ))

    return findings


# ============================================================================
# Analysis Pass 6: Pipeline Detection
# ============================================================================

def detect_pipelines(ctx: ModuleContext) -> List[Finding]:
    """
    Detect pipeline structures (valid shifting, stage registers).

    Pattern: signal_stageN <= signal_stageN-1 in always_ff
    Or: valid chain (valid_s1 <= valid_s0, valid_s2 <= valid_s1, ...)
    """
    findings = []

    # Look for sequential chains: foo_1 <= foo_0, foo_2 <= foo_1, etc.
    # Or: foo[1] <= foo[0], foo[2] <= foo[1]
    chain_pattern = re.compile(
        r'(\w+?)(\d+)\s*<=\s*\1(\d+)',
        re.MULTILINE,
    )
    # Also: foo_s1 <= foo_s0 pattern
    stage_pattern = re.compile(
        r'(\w+?)_?[sS](\d+)\s*<=\s*\1_?[sS](\d+)',
        re.MULTILINE,
    )

    for ab in ctx.always_blocks:
        body = ab.get("body", "")
        line = ab.get("line", 0)
        ab_type = ab.get("type", "always")

        if ab_type not in ("always_ff", "always") or not re.search(r'posedge|negedge', ab.get("sensitivity", "")):
            continue

        chains: Dict[str, List[Tuple[int, int]]] = {}
        for pattern in [chain_pattern, stage_pattern]:
            for cm in pattern.finditer(body):
                base = cm.group(1)
                dst_stage = int(cm.group(2))
                src_stage = int(cm.group(3))
                if dst_stage == src_stage + 1:
                    chains.setdefault(base, []).append((src_stage, dst_stage))

        for base, stages in chains.items():
            if len(stages) >= 2:
                max_stage = max(s[1] for s in stages)
                findings.append(Finding(
                    label="pipeline",
                    category="datapath",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=0.88,
                    signals=[f"{base}{s[1]}" for s in stages],
                    line_numbers=[line],
                    properties={
                        "base_signal": base,
                        "stages": max_stage + 1,
                        "pairs": stages,
                    },
                    description=f"Pipeline chain '{base}' with {max_stage + 1} stages",
                ))

    return findings


# ============================================================================
# Analysis Pass 7: Memory / Register Bank Detection
# ============================================================================

def detect_memories(ctx: ModuleContext) -> List[Finding]:
    """
    Detect register banks, memories, and address decode logic.

    Patterns:
    - logic [W-1:0] mem [0:D-1] → memory array
    - case(addr) with write strobes → register bank
    - Large arrays of registers
    """
    findings = []

    # Detect memory arrays: logic [W:0] name [D:0] or reg [W:0] name [0:D]
    mem_pattern = re.compile(
        r'(?:logic|reg)\s+(\[.*?\])\s+(\w+)\s*(\[.*?\])',
        re.MULTILINE,
    )
    for mm in mem_pattern.finditer(ctx.raw_content):
        width_str = mm.group(1)
        name = mm.group(2)
        depth_str = mm.group(3)

        findings.append(Finding(
            label="memory_array",
            category="storage",
            module=ctx.name,
            file=ctx.file,
            confidence=0.90,
            signals=[name],
            properties={
                "width": width_str,
                "depth": depth_str,
            },
            description=f"Memory array '{name}' {width_str} × {depth_str}",
        ))

    # Detect address decode (case on addr-like signal with write enable)
    for ab in ctx.always_blocks:
        body = ab.get("body", "")
        line = ab.get("line", 0)

        addr_case = re.search(
            r'case[xz]?\s*\(\s*(\w*addr\w*|\w*address\w*)\s*\)',
            body,
            re.IGNORECASE,
        )
        has_write = bool(re.search(r'wr|write|we|wen', body, re.IGNORECASE))

        if addr_case and has_write:
            addr_sig = addr_case.group(1)
            # Count case branches to estimate register count
            endcase_match = re.search(r'\bendcase\b', body[addr_case.start():])
            if endcase_match:
                case_block = body[addr_case.start():addr_case.start() + endcase_match.end()]
                hex_addrs = re.findall(r"(\d+)'h([0-9a-fA-F]+)", case_block)
                reg_count = len(hex_addrs) or len(re.findall(r'^\s*\w+\s*:', case_block, re.MULTILINE))

                findings.append(Finding(
                    label="register_bank",
                    category="storage",
                    module=ctx.name,
                    file=ctx.file,
                    confidence=0.88,
                    signals=[addr_sig],
                    line_numbers=[line],
                    properties={
                        "register_count": reg_count,
                        "addresses": [f"0x{h}" for _, h in hex_addrs[:20]],
                    },
                    description=f"Register bank with address decode on '{addr_sig}' (~{reg_count} registers)",
                ))

    return findings


# ============================================================================
# Analysis Pass 8: FIFO Detection
# ============================================================================

def detect_fifos(ctx: ModuleContext) -> List[Finding]:
    """
    Detect FIFO patterns (sync and async).

    Sync FIFO: wr_ptr, rd_ptr, single clock, full/empty flags
    Async FIFO: wr_ptr + rd_ptr in different clock domains, gray code
    """
    findings = []

    sigs = set(ctx.signals.keys()) | set(ctx.ports.keys())
    sigs_lower = {s.lower(): s for s in sigs}

    has_wr_ptr = any('wr_ptr' in s or 'wptr' in s or 'write_ptr' in s for s in sigs_lower)
    has_rd_ptr = any('rd_ptr' in s or 'rptr' in s or 'read_ptr' in s for s in sigs_lower)
    has_full = any('full' in s for s in sigs_lower)
    has_empty = any('empty' in s for s in sigs_lower)
    has_gray = any('gray' in s or 'grey' in s for s in sigs_lower)
    has_fifo_name = 'fifo' in ctx.name.lower()

    indicators = sum([has_wr_ptr, has_rd_ptr, has_full, has_empty, has_fifo_name])

    if indicators >= 3 or (has_fifo_name and indicators >= 2):
        variant = "async_fifo" if has_gray else "sync_fifo"
        fifo_signals = [
            sigs_lower[s] for s in sigs_lower
            if any(k in s for k in ['ptr', 'full', 'empty', 'fifo', 'gray', 'grey', 'count', 'depth'])
        ]

        findings.append(Finding(
            label=variant,
            category="storage",
            module=ctx.name,
            file=ctx.file,
            confidence=min(0.5 + indicators * 0.12, 0.95),
            signals=fifo_signals[:15],
            properties={
                "has_gray_code": has_gray,
                "has_wr_ptr": has_wr_ptr,
                "has_rd_ptr": has_rd_ptr,
                "has_full_flag": has_full,
                "has_empty_flag": has_empty,
            },
            description=f"{'Async' if has_gray else 'Sync'} FIFO pattern ({indicators}/5 indicators)",
        ))

    return findings


# ============================================================================
# Analysis Pass 9: Arbiter Detection
# ============================================================================

def detect_arbiters(ctx: ModuleContext) -> List[Finding]:
    """
    Detect arbiter patterns.

    Fixed-priority: grant based on priority encoding
    Round-robin: rotating pointer + grant
    """
    findings = []

    sigs = set(ctx.signals.keys()) | set(ctx.ports.keys())
    sigs_lower = {s.lower(): s for s in sigs}

    has_grant = any('grant' in s for s in sigs_lower)
    has_request = any('req' in s for s in sigs_lower)
    has_arb = 'arb' in ctx.name.lower() or 'arbiter' in ctx.name.lower()
    has_priority = any('pri' in s or 'priority' in s for s in sigs_lower)
    has_rr_ptr = any('rr' in s or 'round' in s or 'robin' in s or 'pointer' in s for s in sigs_lower)

    # Check for rotating pointer pattern in always blocks
    has_ptr_rotation = False
    for ab in ctx.always_blocks:
        body = ab.get("body", "")
        if re.search(r'(\w*ptr\w*|\w*pointer\w*)\s*<=.*\+\s*1', body, re.IGNORECASE):
            has_ptr_rotation = True
            break

    indicators = sum([has_grant, has_request, has_arb])
    if indicators >= 2:
        if has_rr_ptr or has_ptr_rotation:
            arb_type = "round_robin_arbiter"
            extra_conf = 0.1
        elif has_priority:
            arb_type = "fixed_priority_arbiter"
            extra_conf = 0.05
        else:
            arb_type = "arbiter"
            extra_conf = 0.0

        arb_signals = [
            sigs_lower[s] for s in sigs_lower
            if any(k in s for k in ['grant', 'req', 'arb', 'pri', 'mask', 'ptr'])
        ]

        findings.append(Finding(
            label=arb_type,
            category="control_flow",
            module=ctx.name,
            file=ctx.file,
            confidence=min(0.55 + indicators * 0.12 + extra_conf, 0.95),
            signals=arb_signals[:15],
            properties={
                "has_rotating_pointer": has_ptr_rotation or has_rr_ptr,
                "has_priority": has_priority,
            },
            description=f"{arb_type.replace('_', ' ').title()} detected ({indicators} indicators)",
        ))

    return findings


# ============================================================================
# Analysis Pass 10: CDC Detection
# ============================================================================

def detect_cdc(ctx: ModuleContext) -> List[Finding]:
    """
    Detect potential clock domain crossing (CDC) candidates.

    Heuristic: signals sampled in always blocks with different clocks.
    Also flag classic 2-flop synchronizer patterns.
    """
    findings = []

    # Map signals to which clocks they're driven by
    clock_domains: Dict[str, List[str]] = {}  # signal → [clk1, clk2, ...]

    for ab in ctx.always_blocks:
        sensitivity = ab.get("sensitivity", "")
        body = ab.get("body", "")

        # Find clock in this block
        clocks = re.findall(r'(?:posedge|negedge)\s+(\w+)', sensitivity)
        clock_names = [c for c in clocks if re.search(r'clk|clock|ck', c, re.IGNORECASE)]

        if not clock_names:
            clock_names = clocks[:1] if clocks else []

        if not clock_names:
            continue

        clk = clock_names[0]
        driven_signals = _extract_lhs_signals(body)
        for sig in driven_signals:
            clock_domains.setdefault(sig, []).append(clk)

    # Find signals that appear in multiple clock domains
    unique_domains = {sig: list(set(clks)) for sig, clks in clock_domains.items()}
    multi_domain = {sig: clks for sig, clks in unique_domains.items() if len(clks) > 1}

    for sig, clks in multi_domain.items():
        findings.append(Finding(
            label="cdc_candidate",
            category="cdc",
            module=ctx.name,
            file=ctx.file,
            confidence=0.85,
            signals=[sig],
            properties={"clock_domains": clks},
            description=f"Signal '{sig}' appears in {len(clks)} clock domains: {', '.join(clks)}",
        ))

    # Detect 2-flop synchronizer pattern: sig_sync1 <= sig; sig_sync2 <= sig_sync1;
    sync_pattern = re.compile(
        r'(\w+)_(?:sync|meta|ff)(\d*)\s*<=\s*(\w+)',
    )
    sync_chains: Dict[str, List[str]] = {}
    for ab in ctx.always_blocks:
        body = ab.get("body", "")
        for sm in sync_pattern.finditer(body):
            base = sm.group(1)
            src = sm.group(3)
            sync_chains.setdefault(base, []).append(src)

    for base, srcs in sync_chains.items():
        if len(srcs) >= 2:
            findings.append(Finding(
                label="synchronizer",
                category="cdc",
                module=ctx.name,
                file=ctx.file,
                confidence=0.88,
                signals=[base] + srcs,
                description=f"Multi-flop synchronizer for '{base}' ({len(srcs)} stages)",
            ))

    return findings


# ============================================================================
# Master runner — run all analysis passes on a module
# ============================================================================

ALL_PASSES = [
    classify_always_blocks,
    detect_clocks_resets,
    detect_fsms,
    detect_interfaces,
    analyze_drivers,
    detect_pipelines,
    detect_memories,
    detect_fifos,
    detect_arbiters,
    detect_cdc,
]


def analyze_module(ctx: ModuleContext) -> List[Finding]:
    """Run all analysis passes on a single module."""
    all_findings = []
    for pass_fn in ALL_PASSES:
        try:
            findings = pass_fn(ctx)
            all_findings.extend(findings)
        except Exception:
            # Never crash on analysis — skip pass and continue
            continue
    return all_findings


def analyze_codebase(hdl_files: List[Path], project_dir: Path) -> Dict[str, Any]:
    """
    Run full static analysis on all HDL files in a project.

    Returns a structured report with:
    - per-module findings (design cards)
    - project-level summary
    - confidence statistics
    """
    all_findings: List[Finding] = []
    module_summaries: Dict[str, Dict[str, Any]] = {}

    for hdl_file in hdl_files:
        ext = hdl_file.suffix.lower()
        if ext in (".vhd", ".vhdl"):
            continue  # Skip VHDL for now

        try:
            content = hdl_file.read_text(errors="replace")
        except OSError:
            continue

        rel_path = str(hdl_file.relative_to(project_dir))
        contexts = extract_module_contexts(content, rel_path)

        for ctx in contexts:
            module_findings = analyze_module(ctx)
            all_findings.extend(module_findings)

            # Build per-module summary
            categories = {}
            for f in module_findings:
                categories.setdefault(f.category, []).append(f.label)

            module_summaries[ctx.name] = {
                "file": ctx.file,
                "port_count": len(ctx.ports),
                "param_count": len(ctx.params),
                "always_block_count": len(ctx.always_blocks),
                "instance_count": len(ctx.instances),
                "finding_count": len(module_findings),
                "categories": {
                    cat: list(set(labels))
                    for cat, labels in categories.items()
                },
            }

    # Build project-level summary
    total_findings = len(all_findings)
    category_counts: Dict[str, int] = {}
    label_counts: Dict[str, int] = {}
    avg_confidence = 0.0

    for f in all_findings:
        category_counts[f.category] = category_counts.get(f.category, 0) + 1
        label_counts[f.label] = label_counts.get(f.label, 0) + 1
        avg_confidence += f.confidence

    if total_findings > 0:
        avg_confidence /= total_findings

    # Sort findings by confidence (highest first)
    high_confidence = [f for f in all_findings if f.confidence >= 0.85]
    medium_confidence = [f for f in all_findings if 0.70 <= f.confidence < 0.85]
    low_confidence = [f for f in all_findings if f.confidence < 0.70]

    return {
        "summary": {
            "total_findings": total_findings,
            "modules_analyzed": len(module_summaries),
            "avg_confidence": round(avg_confidence, 3),
            "high_confidence_count": len(high_confidence),
            "medium_confidence_count": len(medium_confidence),
            "low_confidence_count": len(low_confidence),
            "category_counts": category_counts,
            "label_counts": label_counts,
        },
        "modules": module_summaries,
        "findings": [f.to_dict() for f in all_findings],
        "_note": (
            "Static analysis via pattern matching — no LLMs. "
            "Confidence scores indicate how many expected conditions were met. "
            "High confidence (>=0.85) findings are very reliable. "
            "Low confidence (<0.70) findings should be verified with read_file."
        ),
    }


# ============================================================================
# Internal helpers
# ============================================================================

def _strip_comments(content: str) -> str:
    """Remove // and /* */ comments."""
    content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
    content = re.sub(r'//[^\n]*', '', content)
    return content


def _extract_lhs_signals(body: str) -> List[str]:
    """Extract signal names from the left-hand side of assignments."""
    # Match: signal <= expr  or  signal = expr  (but not ==)
    pattern = re.compile(r'(\w+)\s*<?=[^=]')
    signals = list(set(m.group(1) for m in pattern.finditer(body)))
    return signals


def _extract_ports_from_module(mod_text: str) -> Dict[str, Dict[str, Any]]:
    """Extract ports from module text."""
    ports = {}
    port_pattern = re.compile(
        r'(input|output|inout)\s+'
        r'(?:(wire|reg|logic|var)\s+)?'
        r'(?:signed\s+)?'
        r'(?:(\w+::)?\w+\s+)?'
        r'(?:(\[.*?\])\s+)?'
        r'(\w+)',
    )
    for pm in port_pattern.finditer(mod_text):
        direction = pm.group(1)
        type_str = pm.group(2) or ""
        width_str = pm.group(4) or ""
        port_name = pm.group(5)
        width = 1
        width_match = re.search(r'\[(\d+):(\d+)\]', width_str)
        if width_match:
            width = abs(int(width_match.group(1)) - int(width_match.group(2))) + 1
        ports[port_name] = {
            "direction": direction,
            "width": width,
            "type_str": type_str,
        }
    return ports


def _extract_params_from_module(mod_text: str) -> Dict[str, str]:
    """Extract parameters from module text."""
    params = {}
    pattern = re.compile(
        r'parameter\s+(?:(?:int|integer|logic|bit)\s+)?'
        r'(?:\[.*?\]\s+)?'
        r'(\w+)\s*=\s*([^,;\)]*(?:\([^)]*\)[^,;\)]*)*)',
    )
    for pm in pattern.finditer(mod_text):
        params[pm.group(1)] = pm.group(2).strip()
    return params


def _extract_always_blocks(mod_text: str, base_line: int) -> List[Dict[str, Any]]:
    """Extract always blocks with their type, sensitivity, and body."""
    blocks = []

    # Match always_ff, always_comb, always_latch, always @(...)
    pattern = re.compile(
        r'\b(always_ff|always_comb|always_latch|always)\s*'
        r'(?:@\s*\(([^)]*)\)\s*)?'
        r'(begin\b.*?end\b|[^;]*;)',
        re.DOTALL,
    )

    for am in pattern.finditer(mod_text):
        ab_type = am.group(1)
        sensitivity = am.group(2) or ""
        body = am.group(3) or ""
        line_in_module = mod_text[:am.start()].count('\n')

        blocks.append({
            "type": ab_type,
            "sensitivity": sensitivity.strip(),
            "body": body,
            "line": base_line + line_in_module,
            "raw": am.group(0)[:300],
        })

    return blocks


def _extract_instances_from_module(
    mod_text: str, base_line: int
) -> List[Dict[str, str]]:
    """Extract module instantiations."""
    instances = []
    keywords = {
        "module", "endmodule", "interface", "endinterface", "class",
        "function", "task", "package", "if", "else", "for", "while",
        "begin", "end", "case", "always", "always_ff", "always_comb",
        "always_latch", "assign", "initial", "generate", "input",
        "output", "inout", "wire", "reg", "logic", "integer", "return",
        "import", "typedef", "enum", "struct", "union", "parameter",
        "localparam",
    }

    pattern = re.compile(
        r'\b(\w+)\s+(?:#\s*\(.*?\)\s+)?(\w+)\s*\(',
        re.DOTALL,
    )

    for im in pattern.finditer(mod_text):
        mod_type = im.group(1)
        inst_name = im.group(2)
        if mod_type.lower() in keywords or inst_name.lower() in keywords:
            continue
        line = base_line + mod_text[:im.start()].count('\n')
        instances.append({
            "module_type": mod_type,
            "instance_name": inst_name,
            "line": str(line),
        })

    return instances


def _extract_assigns(mod_text: str, base_line: int) -> List[Dict[str, Any]]:
    """Extract continuous assign statements."""
    assigns = []
    pattern = re.compile(r'\bassign\s+(\w+)\s*=\s*([^;]+);', re.MULTILINE)
    for am in pattern.finditer(mod_text):
        line = base_line + mod_text[:am.start()].count('\n')
        assigns.append({
            "lhs": am.group(1),
            "rhs": am.group(2).strip()[:200],
            "line": line,
        })
    return assigns


def _extract_internal_signals(mod_text: str) -> Dict[str, Dict[str, Any]]:
    """Extract internal signal/wire/reg declarations (not ports)."""
    signals = {}

    # logic [W:0] name; or reg [W:0] name; or wire [W:0] name;
    pattern = re.compile(
        r'(?:^|\s)(logic|reg|wire|bit)\s+'
        r'(?:signed\s+)?'
        r'(?:(\[.*?\])\s+)?'
        r'(\w+)\s*(?:\[.*?\]\s*)?;',
        re.MULTILINE,
    )

    for sm in pattern.finditer(mod_text):
        sig_type = sm.group(1)
        width_str = sm.group(2) or ""
        name = sm.group(3)
        width = 1
        wm = re.search(r'\[(\d+):(\d+)\]', width_str)
        if wm:
            width = abs(int(wm.group(1)) - int(wm.group(2))) + 1
        signals[name] = {
            "type": sig_type,
            "width": width,
        }

    return signals

