"""RPP template extraction and generation.

Extracts track/FX chain structures from professionally-built .rpp files,
fixes GUIDs with real values from reaper-vstplugins64.ini, and generates
working .rpp files.

Usage:
    from src.composer.templates import extract_template, generate_rpp
    template = extract_template("ejemplos/TU_DIABLO_ITHAN_NY.rpp")
    generate_rpp(template, "output/tu_diablo_fixed.rpp")
"""

from __future__ import annotations

import re
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from rpp import Element, dumps

from src.reaper_builder import PLUGIN_REGISTRY, ALIAS_MAP, vst2_element, vst3_element, VST2_REGISTRY, VST3_REGISTRY, PLUGIN_PRESETS


# ---------------------------------------------------------------------------
# Template data classes
# ---------------------------------------------------------------------------

@dataclass
class PluginTemplate:
    """A plugin (instrument or effect) within a track FX chain."""
    name: str              # Registry key (e.g. "Serum2", "Decapitator")
    path: str             # Filename on disk (e.g. "Serum2.vst3", "Decapitator.dll")
    display_name: str     # Full REAPER display name
    uniqueid_guid: str    # uniqueid{GUID} from registry
    preset_data: list[str] | None = None
    is_vst2: bool = False  # True for SoundToys .dll plugins
    index: int = 0        # Position in FX chain

    def build_element(self) -> Element:
        """Build a VST Element for this plugin."""
        if self.is_vst2:
            return vst2_element(self.display_name, self.path, self.uniqueid_guid, self.preset_data)
        return vst3_element(self.display_name, self.path, self.uniqueid_guid, self.preset_data)


@dataclass
class TrackTemplate:
    """A track with its FX chain, extracted from an RPP template."""
    name: str
    volume: float = 0.85
    pan: float = 0.0
    color: int = 0
    is_bus: bool = False
    plugins: list[PluginTemplate] = field(default_factory=list)


@dataclass
class TemplateProject:
    """A complete project template extracted from an RPP file."""
    name: str
    bpm: float
    key: str = "Am"
    time_sig_num: int = 4
    time_sig_den: int = 4
    tracks: list[TrackTemplate] = field(default_factory=list)

    def generate_rpp(self, output_path: str | Path) -> None:
        """Generate a working .rpp file from this template.

        Uses RPPBuilder to produce REAPER 7.65 format with proper GUIDs,
        FXCHAIN structure, and base64 preset data.
        """
        from src.core.schema import SongDefinition, SongMeta, TrackDef, PluginDef
        from src.reaper_builder import RPPBuilder

        meta = SongMeta(
            bpm=self.bpm,
            key=self.key,
            title=self.name,
            time_sig_num=self.time_sig_num,
            time_sig_den=self.time_sig_den,
        )

        # Convert TrackTemplate → TrackDef
        tracks: list[TrackDef] = []
        for tt in self.tracks:
            # Build PluginDef list for RPPBuilder
            plugin_defs: list[PluginDef] = []
            for pt in tt.plugins:
                plugin_defs.append(PluginDef(
                    name=pt.name,
                    path=pt.path,
                    index=pt.index,
                    preset_data=pt.preset_data,
                ))

            track_def = TrackDef(
                name=tt.name,
                volume=tt.volume,
                pan=tt.pan,
                color=tt.color,
                clips=[],
                plugins=plugin_defs,
            )
            tracks.append(track_def)

        song = SongDefinition(meta=meta, tracks=tracks)
        builder = RPPBuilder(song)
        builder.write(str(output_path))


# ---------------------------------------------------------------------------
# Plugin substitution map (for non-existent plugins in source RPPs)
# ---------------------------------------------------------------------------

# Maps plugin names that appear in source RPPs but don't exist → substitute plugin key
SUBSTITUTE: dict[str, str] = {
    # Transient Master not installed — replace with FabFilter Pro-C 2 (similar transient shaping)
    "Transient Master.dll": "FabFilter_Pro-C_2",
    "NI Transient Master": "FabFilter_Pro-C_2",
    "Transient Master (Native Instruments)": "FabFilter_Pro-C_2",
    # The Glue standalone .dll doesn't exist — use VST2 version from registry
    "Cytomic.dll": "The_Glue",
    # ValhallaDelay_x64.dll not in scan — use ValhallaDelay (VST2)
    "ValhallaDelay_x64.dll": "ValhallaDelay",
    # Pigments.exe wrong extension
    "Pigments.exe": "Pigments",
    # Gullfoss VST2 .dll — use registry keys
    "Gullfoss.dll": "Gullfoss",
    "Gullfoss Master.dll": "Gullfoss_Master",
    "Gullfoss Live.dll": "Gullfoss_Live",
    # VC 160/76 — fix display names from "VC 160 FX (Audified)" → registry keys
    "VC 160.vst3": "VC_160",
    "VC 76.vst3": "VC_76",
    "VC 2A.vst3": "VC_2A",
    # Kontakt 7 — fix display name
    "Kontakt 7.vst3": "Kontakt_7",
    # Source RPP display name → registry key (for non-matching display names)
    "VST3: VC 160 FX (Audified)": "VC_160",
    "VST3: VC 76 FX (Audified)": "VC_76",
    "VST3: VC 2A FX (Audified)": "VC_2A",
    "VST: TheGlue (Cytomic)": "The_Glue",
    "VST: ValhallaDelay x64 (Valhalla DSP)": "ValhallaDelay",
    "VST3: Kontakt 7 (Native Instruments)": "Kontakt_7",
    "VST: Gullfoss Master (Soundtheory)": "Gullfoss_Master",
    "VST: Gullfoss (Soundtheory)": "Gullfoss",
    "VST: Gullfoss Live (Soundtheory)": "Gullfoss_Live",
}


# ---------------------------------------------------------------------------
# GUID lookup helpers
# ---------------------------------------------------------------------------

def _lookup_vst3(name: str) -> tuple[str, str, str] | None:
    """Look up VST3 plugin by registry key or filename."""
    # Try exact key match
    if name in VST3_REGISTRY:
        return VST3_REGISTRY[name]
    # Try filename match
    for display_name, filename, uid_guid in VST3_REGISTRY.values():
        if filename == name:
            return (display_name, filename, uid_guid)
    return None


def _lookup_vst2(name: str) -> tuple[str, str, str] | None:
    """Look up VST2/SoundToys plugin by registry key or filename."""
    # Try exact key match
    if name in VST2_REGISTRY:
        return VST2_REGISTRY[name]
    # Try filename match
    for display_name, filename, uid_guid in VST2_REGISTRY.values():
        if filename == name:
            return (display_name, filename, uid_guid)
    return None


def _make_plugin_template(
    name: str,
    path: str,
    index: int = 0,
) -> PluginTemplate | None:
    """Create a PluginTemplate from plugin name + path, with GUID lookup.

    Handles plugin substitution for non-existent plugins.
    Returns None if the plugin cannot be resolved.
    """
    # Apply substitution if needed
    resolved_name = SUBSTITUTE.get(name, name)
    resolved_path = SUBSTITUTE.get(path, path)

    # Try direct registry key lookup first (covers substituted names)
    direct_entry = PLUGIN_REGISTRY.get(resolved_name) or PLUGIN_REGISTRY.get(resolved_path)
    if direct_entry:
        is_vst2 = direct_entry[2].startswith("<")
        entry = direct_entry
    else:
        # Fall back to path-based detection
        is_vst2 = path.endswith(".dll")
        if is_vst2:
            entry = _lookup_vst2(resolved_path) or _lookup_vst2(resolved_name)
        else:
            entry = _lookup_vst3(resolved_path) or _lookup_vst3(resolved_name)

    if entry:
        display_name, filename, uid_guid = entry
        preset_data = PLUGIN_PRESETS.get((resolved_name, ""))
    else:
        # Unresolved — use name/path as display name with empty GUID
        display_name = f"VST3: {resolved_name}" if not is_vst2 else f"VST: {resolved_name}"
        filename = resolved_path
        uid_guid = ""
        preset_data = None

    return PluginTemplate(
        name=resolved_name,
        path=filename,
        display_name=display_name,
        uniqueid_guid=uid_guid,
        preset_data=preset_data,
        is_vst2=is_vst2,
        index=index,
    )


# ---------------------------------------------------------------------------
# RPP parser (regex-based, handles both RPP library format and raw text)
# ---------------------------------------------------------------------------

# Regex patterns for parsing RPP files
_TRACK_RE = re.compile(r'<TRACK\s+\{[^}]+\}', re.IGNORECASE)
_TRACK_CLOSE_RE = re.compile(r'^\s*</TRACK>', re.IGNORECASE)
_NAME_RE = re.compile(r'^\s*NAME\s+"([^"]*)"', re.IGNORECASE)
_VOLPAN_RE = re.compile(r'^\s*VOLPAN\s+([\d.e+-]+)\s+([\d.e+-]+)', re.IGNORECASE)
_ISBUS_RE = re.compile(r'^\s*ISBUS\s+(\d+)', re.IGNORECASE)
_PEAKCOL_RE = re.compile(r'^\s*PEAKCOL\s+(\d+)', re.IGNORECASE)
_FXCHAIN_RE = re.compile(r'^\s*<FXCHAIN', re.IGNORECASE)
_FXCHAIN_CLOSE_RE = re.compile(r'^\s*</FXCHAIN>', re.IGNORECASE)
# VST element: <VST "display name" "filename" 0 "" uniqueid{GUID} "" [preset lines] >
# The header line ends with "" (closing of last string arg) followed by optional preset lines
# and finally a > closing tag on its own line or after preset content.
# The GUID portion (inside {} or <>) may be prefixed with an id number.
# Examples from real RPPs:
#   <VST "VST: Decapitator (SoundToys)" "Decapitator.dll" 0 "" 1145980753<GUID> ""
#   <VST "VST3: Serum 2" "Serum2.vst3" 0 "" 0<> ""
_VST_RE = re.compile(
    r'<VST\s+"([^"]+)"\s+"([^"]+)"\s+0\s+""\s*([\d<>{}a-fA-F0-9]+)\s*""',
    re.IGNORECASE,
)
_VST_CLOSE_RE = re.compile(r'^\s*</VST>', re.IGNORECASE)
_PARAM_RE = re.compile(r'^\s*PARAM\s+\d+\s+"[^"]*"\s+', re.IGNORECASE)
_TEMPO_RE = re.compile(r'^\s*TEMPO\s+([\d.e+-]+)\s+(\d+)\s+(\d+)', re.IGNORECASE)


def _strip_invalid_params(lines: list[str]) -> list[str]:
    """Remove invalid PARAM lines from preset data lines.

    Claude's RPPs contain lines like:
        PARAM 0 "Drive" 0.35
    which are NOT valid REAPER tokens. REAPER stores parameter values in
    base64 preset data, not as named PARAM lines.
    """
    result: list[str] = []
    for line in lines:
        stripped = line.strip()
        # Skip PARAM lines that match our pattern
        if _PARAM_RE.match(line):
            continue
        result.append(line)
    return result


def _extract_guid(raw: str, filename: str, display_name: str) -> str:
    """Extract clean GUID from raw GUID string.

    Handles formats like:
      - "0<>" → lookup from registry (empty/fake GUID)
      - "1145980753<56535444454341>" → extract <GUID>
      - "691258006{56534558...}" → extract {GUID}
      - plain "0<>" or "0{}" → lookup from registry
    """
    import re as re_module
    raw = raw.strip()

    # Try extract <GUID> pattern (angle brackets)
    m = re_module.search(r'<([^<>]+)>', raw)
    if m:
        guid = m.group(1)
        # Check if it's a real GUID (not just angle bracket chars)
        if guid and guid not in ('<>', '{}', ''):
            return guid

    # Try extract {GUID} pattern (curly braces)
    m = re_module.search(r'\{([^}]+)\}', raw)
    if m:
        guid = m.group(1)
        if guid and guid not in ('{}', ''):
            return guid

    # Empty or fake GUID — lookup from registry
    is_vst2 = filename.endswith(".dll")
    key = _resolve_registry_key(display_name, filename, is_vst2)
    if key:
        if is_vst2:
            entry = VST2_REGISTRY.get(key)
        else:
            entry = VST3_REGISTRY.get(key)
        if entry:
            return entry[2]  # Return the uid_guid from registry

    return raw  # Return as-is if no lookup found


def _parse_vst_block(lines: list[str], start_idx: int) -> tuple[PluginTemplate | None, int]:
    """Parse a VST element starting at start_idx in lines.

    Returns (PluginTemplate, index_of_close_tag) or (None, start_idx) if not a VST block.
    """
    line = lines[start_idx].strip()
    m = _VST_RE.match(line)
    if not m:
        return None, start_idx

    display_name = m.group(1)
    filename = m.group(2)
    raw_guid = m.group(3).strip()

    # Extract clean GUID from various formats:
    # - "0<>" (empty/fake GUID) → replace with registry lookup
    # - "1315270729<>" → registry lookup
    # - "1145980753<56535444454341>" → extract <GUID> portion
    # - "691258006{56534558667350736572756D20320000}" → extract {GUID} portion
    uid_guid = _extract_guid(raw_guid, filename, display_name)

    # Collect preset data lines until closing >VST tag
    preset_lines: list[str] = []
    idx = start_idx + 1
    while idx < len(lines):
        l = lines[idx]
        ls = l.strip()
        if '</VST' in l or ls == '>' or ls.startswith('>'):
            break
        if ls.startswith('<VST') or ls.startswith('<TRACK') or ls.startswith('</TRACK') or ls.startswith('</FXCHAIN'):
            # Hit next element — not a preset line
            break
        # Collect line (strip PARAM lines later)
        preset_lines.append(l)
        idx += 1

    preset_lines = _strip_invalid_params(preset_lines)

    is_vst2 = filename.endswith(".dll")
    registry_key = _resolve_registry_key(display_name, filename, is_vst2)

    if registry_key:
        if is_vst2:
            entry = VST2_REGISTRY.get(registry_key)
        else:
            entry = VST3_REGISTRY.get(registry_key)
        if entry:
            disp, fn, guid = entry
            uid_guid = guid
            display_name = disp
            filename = fn
    else:
        sub_key = SUBSTITUTE.get(filename) or SUBSTITUTE.get(display_name)
        if sub_key:
            sub_entry = PLUGIN_REGISTRY.get(sub_key)
            if sub_entry:
                display_name, filename, uid_guid = sub_entry
                is_vst2 = uid_guid.startswith("<")
                registry_key = sub_key

    is_fake_preset = (
        not preset_lines
        or all(pl.strip() in ("0 0", "0", "") for pl in preset_lines)
    )
    if is_fake_preset and registry_key:
        registry_preset = PLUGIN_PRESETS.get((registry_key, ""))
        if registry_preset:
            preset_lines = registry_preset

    return PluginTemplate(
        name=registry_key or display_name,
        path=filename,
        display_name=display_name,
        uniqueid_guid=uid_guid,
        preset_data=preset_lines if preset_lines else None,
        is_vst2=is_vst2,
        index=0,
    ), idx


def _resolve_registry_key(display_name: str, filename: str, is_vst2: bool) -> str | None:
    """Resolve a plugin display name/filename to a registry key."""
    if is_vst2:
        for key, (disp, fn, guid) in VST2_REGISTRY.items():
            if disp == display_name or fn == filename:
                return key
    else:
        for key, (disp, fn, guid) in VST3_REGISTRY.items():
            if disp == display_name or fn == filename:
                return key
    return None


def _parse_track_block(lines: list[str], start_idx: int) -> tuple[TrackTemplate | None, int]:
    """Parse a TRACK element starting at start_idx in lines.

    Returns (TrackTemplate, index_after_close_tag) or (None, start_idx).
    """
    line = lines[start_idx].strip()
    if not _TRACK_RE.match(line):
        return None, start_idx

    name = ""
    volume = 0.85
    pan = 0.0
    color = 0
    is_bus = False
    plugins: list[PluginTemplate] = []
    plugin_index = 0

    idx = start_idx + 1
    in_fxchain = False
    while idx < len(lines):
        l = lines[idx]
        ls = l.strip()

        if ls.startswith('</TRACK>') or ls.startswith('</TRACK'):
            break

        if in_fxchain:
            if _FXCHAIN_CLOSE_RE.match(ls):
                in_fxchain = False
                idx += 1
                continue

            if _VST_RE.match(ls):
                plugin, new_idx = _parse_vst_block(lines, idx)
                if plugin:
                    plugin.index = plugin_index
                    plugin_index += 1
                    plugins.append(plugin)
                idx = new_idx + 1
                continue
        else:
            if _FXCHAIN_RE.match(ls):
                in_fxchain = True
                idx += 1
                continue

            m = _NAME_RE.match(l)
            if m:
                name = m.group(1)
                idx += 1
                continue

            m = _VOLPAN_RE.match(l)
            if m:
                try:
                    volume = float(m.group(1))
                except ValueError:
                    pass
                try:
                    pan = float(m.group(2))
                except ValueError:
                    pass
                idx += 1
                continue

            m = _PEAKCOL_RE.match(l)
            if m:
                try:
                    color = int(m.group(1))
                except ValueError:
                    pass
                idx += 1
                continue

            m = _ISBUS_RE.match(l)
            if m:
                try:
                    is_bus = int(m.group(1)) == 1
                except Value:
                    pass
                idx += 1
                continue

        idx += 1

    if not name:
        name = "Unnamed Track"

    return TrackTemplate(
        name=name,
        volume=volume,
        pan=pan,
        color=color,
        is_bus=is_bus,
        plugins=plugins,
    ), idx


def extract_template(rpp_path: str | Path) -> TemplateProject:
    """Extract a TemplateProject from an existing .rpp file.

    Parses track structures, FX chains, and plugin configurations,
    applying GUID corrections and stripping invalid PARAM lines.

    Args:
        rpp_path: Path to source .rpp file

    Returns:
        TemplateProject with all tracks and plugins resolved to registry entries

    Raises:
        FileNotFoundError: If rpp_path doesn't exist
        ValueError: If file can't be parsed as RPP
    """
    path = Path(rpp_path)
    if not path.exists():
        raise FileNotFoundError(f"RPP file not found: {path}")

    content = path.read_text(encoding="utf-8")
    lines = content.split('\n')

    # Extract tempo, key from header comments or TEMPO line
    bpm = 120.0
    key = "Am"
    time_sig_num, time_sig_den = 4, 4
    project_name = path.stem

    # Parse TEMPO line
    for i, line in enumerate(lines):
        m = _TEMPO_RE.match(line.strip())
        if m:
            try:
                bpm = float(m.group(1))
            except ValueError:
                pass
            try:
                time_sig_num = int(m.group(2))
                time_sig_den = int(m.group(3))
            except ValueError:
                pass
            break

    # Find project name from comments
    for line in lines:
        if 'PROYECTO:' in line:
            # Extract project name from comment
            m = re.search(r'PROYECTO:\s*(.+?)(?:\n|$)', line)
            if m:
                project_name = m.group(1).strip()
                break

    # Extract tracks
    tracks: list[TrackTemplate] = []
    idx = 0
    while idx < len(lines):
        if _TRACK_RE.match(lines[idx].strip()):
            track, new_idx = _parse_track_block(lines, idx)
            if track:
                tracks.append(track)
            idx = new_idx + 1
        else:
            idx += 1

    return TemplateProject(
        name=project_name,
        bpm=bpm,
        key=key,
        time_sig_num=time_sig_num,
        time_sig_den=time_sig_den,
        tracks=tracks,
    )


def generate_rpp(template: TemplateProject, output_path: str | Path) -> None:
    """Generate a working .rpp file from a TemplateProject.

    Uses RPPBuilder to produce REAPER 7.65 format with correct GUIDs,
    proper FXCHAIN structure, and base64 preset data. All plugins are
    resolved via VST3_REGISTRY/VST2_REGISTRY.

    Args:
        template: TemplateProject to generate from
        output_path: Destination .rpp path
    """
    # Ensure output directory exists
    p = Path(output_path)
    p.parent.mkdir(parents=True, exist_ok=True)

    template.generate_rpp(str(p))
