"""
Recipe Service - business logic for recipe management.

Recipes are S-expressions stored in the content-addressed cache (and IPFS).
The recipe ID is the content hash of the file.
"""

import tempfile
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple, TYPE_CHECKING

from artdag.sexp import compile_string, parse, serialize, CompileError, ParseError

if TYPE_CHECKING:
    import redis
    from cache_manager import L1CacheManager

from ..types import Recipe, CompiledDAG, VisualizationDAG, VisNode, VisEdge


class RecipeService:
    """
    Service for managing recipes.

    Recipes are S-expressions stored in the content-addressed cache.
    """

    def __init__(self, redis: "redis.Redis", cache: "L1CacheManager") -> None:
        # Redis kept for compatibility but not used for recipe storage
        self.redis = redis
        self.cache = cache

    async def get_recipe(self, recipe_id: str) -> Optional[Recipe]:
        """Get a recipe by ID (content hash)."""
        import yaml
        import logging
        logger = logging.getLogger(__name__)

        # Get from cache (content-addressed storage)
        logger.info(f"get_recipe: Looking up recipe_id={recipe_id[:16]}...")
        path = self.cache.get_by_cid(recipe_id)
        logger.info(f"get_recipe: cache.get_by_cid returned path={path}")
        if not path or not path.exists():
            logger.warning(f"get_recipe: Recipe {recipe_id[:16]}... not found in cache")
            return None

        with open(path) as f:
            content = f.read()

        # Detect format - check if it starts with ( after skipping comments
        def is_sexp_format(text):
            for line in text.split('\n'):
                stripped = line.strip()
                if not stripped or stripped.startswith(';'):
                    continue
                return stripped.startswith('(')
            return False

        import logging
        logger = logging.getLogger(__name__)

        if is_sexp_format(content):
            # Detect if this is a streaming recipe (starts with (stream ...))
            def is_streaming_recipe(text):
                for line in text.split('\n'):
                    stripped = line.strip()
                    if not stripped or stripped.startswith(';'):
                        continue
                    return stripped.startswith('(stream')
                return False

            if is_streaming_recipe(content):
                # Streaming recipes have different format - parse manually
                import re
                name_match = re.search(r'\(stream\s+"([^"]+)"', content)
                recipe_name = name_match.group(1) if name_match else "streaming"

                recipe_data = {
                    "name": recipe_name,
                    "sexp": content,
                    "format": "sexp",
                    "type": "streaming",
                    "dag": {"nodes": []},  # Streaming recipes don't have traditional DAG
                }
                logger.info(f"Parsed streaming recipe {recipe_id[:16]}..., name: {recipe_name}")
            else:
                # Parse traditional (recipe ...) S-expression
                try:
                    compiled = compile_string(content)
                    recipe_data = compiled.to_dict()
                    recipe_data["sexp"] = content
                    recipe_data["format"] = "sexp"
                    logger.info(f"Parsed sexp recipe {recipe_id[:16]}..., keys: {list(recipe_data.keys())}")
                except (ParseError, CompileError) as e:
                    logger.warning(f"Failed to parse sexp recipe {recipe_id[:16]}...: {e}")
                    return {"error": str(e), "recipe_id": recipe_id}
        else:
            # Parse YAML
            try:
                recipe_data = yaml.safe_load(content)
                if not isinstance(recipe_data, dict):
                    return {"error": "Invalid YAML: expected dictionary", "recipe_id": recipe_id}
                recipe_data["yaml"] = content
                recipe_data["format"] = "yaml"
            except yaml.YAMLError as e:
                return {"error": f"YAML parse error: {e}", "recipe_id": recipe_id}

        # Add the recipe_id to the data for convenience
        recipe_data["recipe_id"] = recipe_id

        # Get IPFS CID if available
        ipfs_cid = self.cache.get_ipfs_cid(recipe_id)
        if ipfs_cid:
            recipe_data["ipfs_cid"] = ipfs_cid

        # Compute step_count from nodes (handle both formats)
        if recipe_data.get("format") == "sexp":
            nodes = recipe_data.get("dag", {}).get("nodes", [])
        else:
            # YAML format: nodes might be at top level or under dag
            nodes = recipe_data.get("nodes", recipe_data.get("dag", {}).get("nodes", []))
        recipe_data["step_count"] = len(nodes) if isinstance(nodes, (list, dict)) else 0

        return recipe_data

    async def list_recipes(self, actor_id: Optional[str] = None, offset: int = 0, limit: int = 20) -> List[Recipe]:
        """
        List recipes owned by a user.

        Queries item_types table for user's recipe links.
        """
        import logging
        import database
        logger = logging.getLogger(__name__)

        recipes = []

        if not actor_id:
            logger.warning("list_recipes called without actor_id")
            return []

        # Get user's recipe CIDs from item_types
        user_items = await database.get_user_items(actor_id, item_type="recipe", limit=1000)
        recipe_cids = [item["cid"] for item in user_items]
        logger.info(f"Found {len(recipe_cids)} recipe CIDs for user {actor_id}")

        for cid in recipe_cids:
            recipe = await self.get_recipe(cid)
            if recipe and not recipe.get("error"):
                recipes.append(recipe)
            elif recipe and recipe.get("error"):
                logger.warning(f"Recipe {cid[:16]}... has error: {recipe.get('error')}")

        # Add friendly names
        from .naming_service import get_naming_service
        naming = get_naming_service()
        for recipe in recipes:
            recipe_id = recipe.get("recipe_id")
            if recipe_id:
                friendly = await naming.get_by_cid(actor_id, recipe_id)
                if friendly:
                    recipe["friendly_name"] = friendly["friendly_name"]
                    recipe["base_name"] = friendly["base_name"]

        # Sort by name
        recipes.sort(key=lambda r: r.get("name", ""))

        return recipes[offset:offset + limit]

    async def upload_recipe(
        self,
        content: str,
        uploader: str,
        name: str = None,
        description: str = None,
    ) -> Tuple[Optional[str], Optional[str]]:
        """
        Upload a recipe from S-expression content.

        The recipe is stored in the cache and pinned to IPFS.
        Returns (recipe_id, error_message).
        """
        # Validate S-expression
        try:
            compiled = compile_string(content)
        except ParseError as e:
            return None, f"Parse error: {e}"
        except CompileError as e:
            return None, f"Compile error: {e}"

        # Write to temp file for caching
        import logging
        logger = logging.getLogger(__name__)
        try:
            with tempfile.NamedTemporaryFile(delete=False, suffix=".sexp", mode="w") as tmp:
                tmp.write(content)
                tmp_path = Path(tmp.name)

            # Store in cache (content-addressed, auto-pins to IPFS)
            logger.info(f"upload_recipe: Storing recipe in cache from {tmp_path}")
            cached, ipfs_cid = self.cache.put(tmp_path, node_type="recipe", move=True)
            recipe_id = ipfs_cid or cached.cid  # Prefer IPFS CID
            logger.info(f"upload_recipe: Stored recipe, cached.cid={cached.cid[:16]}..., ipfs_cid={ipfs_cid[:16] if ipfs_cid else None}, recipe_id={recipe_id[:16]}...")

            # Track ownership in item_types and assign friendly name
            if uploader:
                import database
                display_name = name or compiled.name or "unnamed-recipe"

                # Create item_types entry (ownership link)
                await database.save_item_metadata(
                    cid=recipe_id,
                    actor_id=uploader,
                    item_type="recipe",
                    description=description,
                    filename=f"{display_name}.sexp",
                )

                # Assign friendly name
                from .naming_service import get_naming_service
                naming = get_naming_service()
                await naming.assign_name(
                    cid=recipe_id,
                    actor_id=uploader,
                    item_type="recipe",
                    display_name=display_name,
                )

            return recipe_id, None

        except Exception as e:
            return None, f"Failed to cache recipe: {e}"

    async def delete_recipe(self, recipe_id: str, actor_id: str = None) -> Tuple[bool, Optional[str]]:
        """
        Remove user's ownership link to a recipe.

        This removes the item_types entry linking the user to the recipe.
        The cached file is only deleted if no other users own it.
        Returns (success, error_message).
        """
        import database

        if not actor_id:
            return False, "actor_id required"

        # Remove user's ownership link
        try:
            await database.delete_item_type(recipe_id, actor_id, "recipe")

            # Also remove friendly name
            await database.delete_friendly_name(actor_id, recipe_id)

            # Try to garbage collect if no one owns it anymore
            # (delete_cache_item only deletes if no item_types remain)
            await database.delete_cache_item(recipe_id)

            return True, None
        except Exception as e:
            return False, f"Failed to delete: {e}"

    def parse_recipe(self, content: str) -> CompiledDAG:
        """Parse recipe S-expression content."""
        compiled = compile_string(content)
        return compiled.to_dict()

    def build_dag(self, recipe: Recipe) -> VisualizationDAG:
        """
        Build DAG visualization data from recipe.

        Returns nodes and edges for Cytoscape.js.
        """
        vis_nodes: List[VisNode] = []
        edges: List[VisEdge] = []

        dag = recipe.get("dag", {})
        dag_nodes = dag.get("nodes", [])
        output_node = dag.get("output")

        # Handle list format (compiled S-expression)
        if isinstance(dag_nodes, list):
            for node_def in dag_nodes:
                node_id = node_def.get("id")
                node_type = node_def.get("type", "EFFECT")

                vis_nodes.append({
                    "data": {
                        "id": node_id,
                        "label": node_id,
                        "nodeType": node_type,
                        "isOutput": node_id == output_node,
                    }
                })

                for input_ref in node_def.get("inputs", []):
                    if isinstance(input_ref, dict):
                        source = input_ref.get("node") or input_ref.get("input")
                    else:
                        source = input_ref

                    if source:
                        edges.append({
                            "data": {
                                "source": source,
                                "target": node_id,
                            }
                        })

        # Handle dict format
        elif isinstance(dag_nodes, dict):
            for node_id, node_def in dag_nodes.items():
                node_type = node_def.get("type", "EFFECT")

                vis_nodes.append({
                    "data": {
                        "id": node_id,
                        "label": node_id,
                        "nodeType": node_type,
                        "isOutput": node_id == output_node,
                    }
                })

                for input_ref in node_def.get("inputs", []):
                    if isinstance(input_ref, dict):
                        source = input_ref.get("node") or input_ref.get("input")
                    else:
                        source = input_ref

                    if source:
                        edges.append({
                            "data": {
                                "source": source,
                                "target": node_id,
                            }
                        })

        return {"nodes": vis_nodes, "edges": edges}
