"""Text-based .rpp structural validator.

Validates: track count, audio clip paths, MIDI note presence,
arrangement duration, send routing, and plugin chain presence.
"""

from __future__ import annotations

import re
from pathlib import Path

# Return track names that do NOT need sends or FXCHAIN
_RETURN_TRACK_NAMES = frozenset({"Reverb", "Delay"})


def _find_track_name(block: str) -> str | None:
    """Extract track NAME from a <TRACK> block.

    Handles two formats found in real .rpp files:
    - Inline: '<TRACK ... NAME "Drumloop"'
    - Separate: '\n    NAME "808 Bass"'
    """
    m = re.search(r'<NAME "([^"]+)"', block)
    if m:
        return m.group(1)
    # Fallback: NAME without < prefix (e.g., '    NAME "master"')
    m = re.search(r'^NAME "([^"]+)"', block, re.MULTILINE)
    if m:
        return m.group(1)
    return None


def validate_rpp_output(
    rpp_path: str,
    expected_bpm: float = 0,
    expected_bars: int = 0,
) -> list[str]:
    """Validate a generated .rpp file. Returns list of error strings (empty = valid)."""
    errors = []
    content = Path(rpp_path).read_text(encoding="utf-8")
    lines = content.split("\n")

    # -------------------------------------------------------------------------
    # 1. Track count — count <TRACK> opening lines (9 = 7 normal + 2 return,
    #    plus 1 master track that REAPER adds = 10 total in our compose output)
    # -------------------------------------------------------------------------
    track_count = sum(1 for line in lines if line.lstrip().startswith("<TRACK"))
    if track_count < 9:
        errors.append(f"Expected 9+ tracks, got {track_count}")
    # Don't enforce upper bound since master track can push count to 10

    # -------------------------------------------------------------------------
    # 2. Audio clip paths — verify <SOURCE WAVE> FILE entries exist
    # Pattern: <SOURCE WAVE followed by FILE "path"
    # -------------------------------------------------------------------------
    wave_source_blocks = re.finditer(r"<SOURCE WAVE\b", content)
    for match in wave_source_blocks:
        start = match.end()
        # Look for FILE on the same line or next line
        rest = content[start : start + 200]
        file_match = re.search(r'FILE "([^"]+)"', rest)
        if file_match:
            path = file_match.group(1)
            if not Path(path).exists():
                errors.append(f"Audio clip path does not exist: {path}")

    # -------------------------------------------------------------------------
    # 3. MIDI clips have notes — look for <SOURCE MIDI> blocks with E events
    # Real format: E 0.0 90 21 54  (timestamp pitch velocity extra)
    # Note-on: pitch byte 0x90, velocity in 0-127
    # -------------------------------------------------------------------------
    midi_source_blocks = re.finditer(r"<SOURCE MIDI\b", content)
    for match in midi_source_blocks:
        start = match.start()
        # Get the rest of this block (until closing >)
        end_match = re.search(r">", content[start:])
        if not end_match:
            continue
        block_end = start + end_match.end()
        block = content[start:block_end]

        # Count E (MIDI event) lines with format: E <float> <hex> <hex>
        # E lines start with whitespace, not column 0 (indented within <SOURCE MIDI block)
        # Pattern captures: E <timestamp> <pitch> <velocity> where pitch is 0x90 (note-on)
        event_lines = re.findall(r"^\s+E\s+\d+\.\d+\s+[0-9a-fA-F]{2}\s+[0-9a-fA-F]{2}", block, re.MULTILINE)
        if len(event_lines) == 0:
            errors.append("MIDI clip has no notes")

    # -------------------------------------------------------------------------
    # 4. Plugin chains present — count VST entries (VST or VST3)
    # -------------------------------------------------------------------------
    vst_matches = re.findall(r"<VST\b", content)
    if len(vst_matches) < 19:
        errors.append(f"Expected 19+ VST plugins, got {len(vst_matches)}")

    # -------------------------------------------------------------------------
    # 5. Send routing wired — each non-return track must have AUXRECV
    # -------------------------------------------------------------------------
    # Split content into track blocks by <TRACK> markers
    track_starts = [m.start() for m in re.finditer(r"<TRACK\b", content)]
    track_blocks: list[tuple[int, str]] = []
    for i, ts in enumerate(track_starts):
        te = track_starts[i + 1] if i + 1 < len(track_starts) else len(content)
        track_blocks.append((ts, content[ts:te]))

    for ts, block in track_blocks:
        name = _find_track_name(block)
        if name is None:
            continue

        # Skip return tracks
        if name in _RETURN_TRACK_NAMES:
            continue

        # Non-return tracks need AUXRECV
        if "AUXRECV" not in block:
            errors.append(f"Track '{name}' missing send to return track")

    # -------------------------------------------------------------------------
    # 6. Arrangement duration — find max ITEM end position
    # Verify the project spans at least expected_bars (default 52 bars)
    # -------------------------------------------------------------------------
    if expected_bars > 0:
        # Find all ITEM blocks - extract POSITION and LENGTH from block
        # Items are multi-line, inside <ITEM>...</ITEM> structure
        max_end = 0.0
        for item_match in re.finditer(r"<ITEM\b", content):
            item_start = item_match.start()
            # Find the closing > for this ITEM
            rest = content[item_match.end() : item_match.end() + 500]
            close_match = re.search(r">", rest)
            if not close_match:
                continue
            block_end_pos = item_match.end() + close_match.end()
            block = content[item_start:block_end_pos]

            # Extract POSITION and LENGTH
            pos_match = re.search(r"POSITION\s+([\d.]+)", block)
            len_match = re.search(r"LENGTH\s+([\d.]+)", block)
            if pos_match and len_match:
                pos = float(pos_match.group(1))
                length = float(len_match.group(1))
                end = pos + length
                if end > max_end:
                    max_end = end

        expected_beats = expected_bars * 4.0
        if max_end < expected_beats:
            errors.append(
                f"Arrangement ends at beat {max_end:.1f}, expected at least {expected_beats:.1f}"
            )

    return errors