"""
Adapter to run sexp recipes through the streaming compositor.

Bridges the gap between:
- Existing recipe format (sexp files with stages, effects, analysis)
- Streaming compositor (sources, effect chains, compositor config)
"""

import sys
from pathlib import Path
from typing import Dict, List, Any, Optional

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "artdag"))

from .compositor import StreamingCompositor
from .sources import VideoSource
from .audio import FileAudioAnalyzer


class RecipeAdapter:
    """
    Adapts a compiled sexp recipe to run through the streaming compositor.

    Example:
        adapter = RecipeAdapter("effects/quick_test.sexp")
        adapter.run(output="preview", duration=60)
    """

    def __init__(
        self,
        recipe_path: str,
        params: Dict[str, Any] = None,
        backend: str = "numpy",
    ):
        """
        Load and prepare a recipe for streaming.

        Args:
            recipe_path: Path to .sexp recipe file
            params: Parameter overrides
            backend: "numpy" or "glsl"
        """
        self.recipe_path = Path(recipe_path)
        self.recipe_dir = self.recipe_path.parent
        self.params = params or {}
        self.backend = backend

        # Compile recipe
        self._compile()

    def _compile(self):
        """Compile the recipe and extract structure."""
        from artdag.sexp.compiler import compile_string

        recipe_text = self.recipe_path.read_text()
        self.compiled = compile_string(recipe_text, self.params, recipe_dir=self.recipe_dir)

        # Extract key info
        self.sources = {}  # name -> path
        self.effects_registry = {}  # effect_name -> path
        self.analyzers = {}  # name -> analyzer info

        # Walk nodes to find sources and structure
        # nodes is a list in CompiledRecipe
        for node in self.compiled.nodes:
            node_type = node.get("type", "")

            if node_type == "SOURCE":
                config = node.get("config", {})
                path = config.get("path")
                if path:
                    self.sources[node["id"]] = self.recipe_dir / path

            elif node_type == "ANALYZE":
                config = node.get("config", {})
                self.analyzers[node["id"]] = {
                    "analyzer": config.get("analyzer"),
                    "path": config.get("analyzer_path"),
                }

        # Get effects registry from compiled recipe
        # registry has 'effects' sub-dict
        effects_dict = self.compiled.registry.get("effects", {})
        for name, info in effects_dict.items():
            if info.get("path"):
                self.effects_registry[name] = Path(info["path"])

    def run_analysis(self) -> Dict[str, Any]:
        """
        Run analysis phase (energy, beats, etc.).

        Returns:
            Dict of analysis track name -> {times, values, duration}
        """
        print(f"Running analysis...", file=sys.stderr)

        # Use existing planner's analysis execution
        from artdag.sexp.planner import create_plan

        analysis_data = {}

        def on_analysis(node_id: str, results: dict):
            analysis_data[node_id] = results
            print(f"  {node_id[:16]}...: {len(results.get('times', []))} samples", file=sys.stderr)

        # Create plan (runs analysis as side effect)
        plan = create_plan(
            self.compiled,
            inputs={},
            recipe_dir=self.recipe_dir,
            on_analysis=on_analysis,
        )

        # Also store named analysis tracks
        for name, data in plan.analysis.items():
            analysis_data[name] = data

        return analysis_data

    def build_compositor(
        self,
        analysis_data: Dict[str, Any] = None,
        fps: float = None,
    ) -> StreamingCompositor:
        """
        Build a streaming compositor from the recipe.

        This is a simplified version that handles common patterns.
        Complex recipes may need manual configuration.

        Args:
            analysis_data: Pre-computed analysis data

        Returns:
            Configured StreamingCompositor
        """
        # Extract video and audio sources in SLICE_ON input order
        video_sources = []
        audio_source = None

        # Find audio source first
        for node_id, path in self.sources.items():
            suffix = path.suffix.lower()
            if suffix in ('.mp3', '.wav', '.flac', '.ogg', '.m4a', '.aac'):
                audio_source = str(path)
                break

        # Find SLICE_ON node to get correct video order
        slice_on_inputs = None
        for node in self.compiled.nodes:
            if node.get('type') == 'SLICE_ON':
                # Use 'videos' config key which has the correct order
                config = node.get('config', {})
                slice_on_inputs = config.get('videos', [])
                break

        if slice_on_inputs:
            # Trace each SLICE_ON input back to its SOURCE
            node_lookup = {n['id']: n for n in self.compiled.nodes}

            def trace_to_source(node_id, visited=None):
                """Trace a node back to its SOURCE, return source path."""
                if visited is None:
                    visited = set()
                if node_id in visited:
                    return None
                visited.add(node_id)

                node = node_lookup.get(node_id)
                if not node:
                    return None
                if node.get('type') == 'SOURCE':
                    return self.sources.get(node_id)
                # Recurse through inputs
                for inp in node.get('inputs', []):
                    result = trace_to_source(inp, visited)
                    if result:
                        return result
                return None

            # Build video_sources in SLICE_ON input order
            for inp_id in slice_on_inputs:
                source_path = trace_to_source(inp_id)
                if source_path:
                    suffix = source_path.suffix.lower()
                    if suffix in ('.mp4', '.webm', '.mov', '.avi', '.mkv'):
                        video_sources.append(str(source_path))

        # Fallback to definition order if no SLICE_ON
        if not video_sources:
            for node_id, path in self.sources.items():
                suffix = path.suffix.lower()
                if suffix in ('.mp4', '.webm', '.mov', '.avi', '.mkv'):
                    video_sources.append(str(path))

        if not video_sources:
            raise ValueError("No video sources found in recipe")

        # Build effect chains - use live audio bindings (matching video_sources count)
        effects_per_source = self._build_streaming_effects(n_sources=len(video_sources))

        # Build compositor config from recipe
        compositor_config = self._extract_compositor_config(analysis_data)

        return StreamingCompositor(
            sources=video_sources,
            effects_per_source=effects_per_source,
            compositor_config=compositor_config,
            analysis_data=analysis_data or {},
            backend=self.backend,
            recipe_dir=self.recipe_dir,
            fps=fps or self.compiled.encoding.get("fps", 30),
            audio_source=audio_source,
        )

    def _build_streaming_effects(self, n_sources: int = None) -> List[List[Dict]]:
        """
        Build effect chains for streaming with live audio bindings.

        Replicates the recipe's effect pipeline:
        - Per source: rotate, zoom, invert, hue_shift, ascii_art
        - All driven by live_energy and live_beat
        """
        if n_sources is None:
            n_sources = len([p for p in self.sources.values()
                            if p.suffix.lower() in ('.mp4', '.webm', '.mov', '.avi', '.mkv')])

        effects_per_source = []

        for i in range(n_sources):
            # Alternate rotation direction per source
            rot_dir = 1 if i % 2 == 0 else -1

            effects = [
                # Rotate - energy drives angle
                {
                    "effect": "rotate",
                    "effect_path": str(self.effects_registry.get("rotate", "")),
                    "angle": {
                        "_binding": True,
                        "source": "live_energy",
                        "feature": "values",
                        "range": [0, 45 * rot_dir],
                    },
                },
                # Zoom - energy drives amount
                {
                    "effect": "zoom",
                    "effect_path": str(self.effects_registry.get("zoom", "")),
                    "amount": {
                        "_binding": True,
                        "source": "live_energy",
                        "feature": "values",
                        "range": [1.0, 1.5] if i % 2 == 0 else [1.0, 0.7],
                    },
                },
                # Invert - beat triggers
                {
                    "effect": "invert",
                    "effect_path": str(self.effects_registry.get("invert", "")),
                    "amount": {
                        "_binding": True,
                        "source": "live_beat",
                        "feature": "values",
                        "range": [0, 1],
                    },
                },
                # Hue shift - energy drives hue
                {
                    "effect": "hue_shift",
                    "effect_path": str(self.effects_registry.get("hue_shift", "")),
                    "degrees": {
                        "_binding": True,
                        "source": "live_energy",
                        "feature": "values",
                        "range": [0, 180],
                    },
                },
                # ASCII art - energy drives char size, beat triggers mix
                {
                    "effect": "ascii_art",
                    "effect_path": str(self.effects_registry.get("ascii_art", "")),
                    "char_size": {
                        "_binding": True,
                        "source": "live_energy",
                        "feature": "values",
                        "range": [4, 32],
                    },
                    "mix": {
                        "_binding": True,
                        "source": "live_beat",
                        "feature": "values",
                        "range": [0, 1],
                    },
                },
            ]
            effects_per_source.append(effects)

        return effects_per_source

    def _extract_effects(self) -> List[List[Dict]]:
        """Extract effect chains for each source (legacy, pre-computed analysis)."""
        # Simplified: find EFFECT nodes and their configs
        effects_per_source = []

        for node_id, path in self.sources.items():
            if path.suffix.lower() not in ('.mp4', '.webm', '.mov', '.avi', '.mkv'):
                continue

            # Find effects that depend on this source
            # This is simplified - real implementation would trace the DAG
            effects = []

            for node in self.compiled.nodes:
                if node.get("type") == "EFFECT":
                    config = node.get("config", {})
                    effect_name = config.get("effect")
                    if effect_name and effect_name in self.effects_registry:
                        effect_config = {
                            "effect": effect_name,
                            "effect_path": str(self.effects_registry[effect_name]),
                        }
                        # Copy only effect params (filter out internal fields)
                        internal_fields = (
                            "effect", "effect_path", "cid", "effect_cid",
                            "effects_registry", "analysis_refs", "inputs",
                        )
                        for k, v in config.items():
                            if k not in internal_fields:
                                effect_config[k] = v
                        effects.append(effect_config)
                        break  # One effect per source for now

            effects_per_source.append(effects)

        return effects_per_source

    def _extract_compositor_config(self, analysis_data: Dict) -> Dict:
        """Extract compositor configuration."""
        # Look for blend_multi or similar composition nodes
        for node in self.compiled.nodes:
            if node.get("type") == "EFFECT":
                config = node.get("config", {})
                if config.get("effect") == "blend_multi":
                    return {
                        "mode": config.get("mode", "alpha"),
                        "weights": config.get("weights", []),
                    }

        # Default: equal blend
        n_sources = len([p for p in self.sources.values()
                        if p.suffix.lower() in ('.mp4', '.webm', '.mov', '.avi', '.mkv')])
        return {
            "mode": "alpha",
            "weights": [1.0 / n_sources] * n_sources if n_sources > 0 else [1.0],
        }

    def run(
        self,
        output: str = "preview",
        duration: float = None,
        fps: float = None,
    ):
        """
        Run the recipe through streaming compositor.

        Everything streams: video frames read on-demand, audio analyzed in real-time.
        No pre-computation.

        Args:
            output: "preview", filename, or Output object
            duration: Duration in seconds (default: audio duration)
            fps: Frame rate (default from recipe, or 30)
        """
        # Build compositor with recipe executor for full pipeline
        from .recipe_executor import StreamingRecipeExecutor

        compositor = self.build_compositor(analysis_data={}, fps=fps)

        # Use audio duration if not specified
        if duration is None:
            if compositor._audio_analyzer:
                duration = compositor._audio_analyzer.duration
                print(f"Using audio duration: {duration:.1f}s", file=sys.stderr)
            else:
                # Live mode - run until quit
                print("Live mode - press 'q' to quit", file=sys.stderr)

        # Create sexp executor that interprets the recipe
        from .sexp_executor import SexpStreamingExecutor
        executor = SexpStreamingExecutor(self.compiled, seed=42)

        compositor.run(output=output, duration=duration, recipe_executor=executor)


def run_recipe(
    recipe_path: str,
    output: str = "preview",
    duration: float = None,
    params: Dict = None,
    fps: float = None,
):
    """
    Run a recipe through streaming compositor.

    Everything streams in real-time: video frames, audio analysis.
    No pre-computation - starts immediately.

    Example:
        run_recipe("effects/quick_test.sexp", output="preview", duration=30)
        run_recipe("effects/quick_test.sexp", output="preview", fps=5)  # Lower fps for slow systems
    """
    adapter = RecipeAdapter(recipe_path, params=params)
    adapter.run(output=output, duration=duration, fps=fps)


def run_recipe_piped(
    recipe_path: str,
    duration: float = None,
    params: Dict = None,
    fps: float = None,
):
    """
    Run recipe and pipe directly to mpv.
    """
    from .output import PipeOutput

    adapter = RecipeAdapter(recipe_path, params=params)
    compositor = adapter.build_compositor(analysis_data={}, fps=fps)

    # Get frame size
    if compositor.sources:
        first_source = compositor.sources[0]
        w, h = first_source._size
    else:
        w, h = 720, 720

    actual_fps = fps or adapter.compiled.encoding.get('fps', 30)

    # Create pipe output
    pipe_out = PipeOutput(
        size=(w, h),
        fps=actual_fps,
        audio_source=compositor._audio_source
    )

    # Create executor
    from .sexp_executor import SexpStreamingExecutor
    executor = SexpStreamingExecutor(adapter.compiled, seed=42)

    # Run with pipe output
    compositor.run(output=pipe_out, duration=duration, recipe_executor=executor)


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Run sexp recipe with streaming compositor")
    parser.add_argument("recipe", help="Path to .sexp recipe file")
    parser.add_argument("-o", "--output", default="pipe",
                       help="Output: 'pipe' (mpv), 'preview', or filename (default: pipe)")
    parser.add_argument("-d", "--duration", type=float, default=None,
                       help="Duration in seconds (default: audio duration)")
    parser.add_argument("--fps", type=float, default=None,
                       help="Frame rate (default: from recipe)")
    args = parser.parse_args()

    if args.output == "pipe":
        run_recipe_piped(args.recipe, duration=args.duration, fps=args.fps)
    else:
        run_recipe(args.recipe, output=args.output, duration=args.duration, fps=args.fps)
