"""
StrategyLifecycleManager — manages running strategy subprocesses.

Responsibilities:
- Starting strategy subprocesses from git worktrees
- Stopping subprocesses on deactivation
- Persisting state to SQLite for crash recovery
- Registering strategies as LifecycleManager triggers (prevents idle shutdown)
- Enforcing max concurrent strategy limit
"""
from __future__ import annotations

import asyncio
import json
import logging
import threading
import time
from pathlib import Path
from typing import Optional

log = logging.getLogger(__name__)

MAX_CONCURRENT_STRATEGIES = 5
DEFAULT_POLL_INTERVAL = 60  # seconds between bar checks


class StrategyLifecycleManager:

    def __init__(self, data_dir: Path, event_bridge=None, lifecycle_manager=None):
        self.data_dir = data_dir
        self.worktrees_dir = data_dir / "worktrees"
        self.configs_dir = data_dir / "strategy_configs"
        self._bridge = event_bridge
        self._lifecycle = lifecycle_manager  # dexorder LifecycleManager
        self._runners: dict[str, tuple[threading.Thread, threading.Event]] = {}  # name -> (thread, stop_event)
        self._db: Optional["StrategyDB"] = None

    async def initialize(self) -> None:
        """Initialize DB and prune stale worktrees."""
        from dexorder.strategy.db import get_strategy_db
        from dexorder.tools.python_tools import get_category_manager

        self._db = get_strategy_db(self.data_dir)
        await self._db.initialize()
        self.worktrees_dir.mkdir(parents=True, exist_ok=True)
        self.configs_dir.mkdir(parents=True, exist_ok=True)

        # Prune any git worktrees that are no longer registered
        try:
            mgr = get_category_manager(self.data_dir)
            mgr.git.prune_worktrees()
        except Exception as e:
            log.warning("git worktree prune failed: %s", e)

    async def resume_running(self) -> None:
        """On container restart, re-launch strategies that were 'running' at shutdown."""
        if self._db is None:
            return
        try:
            running = await self._db.get_running_strategies()
            for row in running:
                name = row["name"]
                log.info("Resuming strategy '%s' after container restart", name)
                feeds = json.loads(row.get("feeds_json") or "[]")
                await self.activate(
                    strategy_name=name,
                    feeds=feeds,
                    allocation=row.get("allocation", 10_000.0),
                    paper=bool(row.get("paper", 1)),
                    _resume=True,
                )
        except Exception as e:
            log.error("Failed to resume strategies: %s", e)

    # ------------------------------------------------------------------
    # Activate / Deactivate
    # ------------------------------------------------------------------

    async def activate(
        self,
        strategy_name: str,
        feeds: list[dict],
        allocation: float,
        paper: bool = True,
        git_revision: str = "HEAD",
        _resume: bool = False,
    ) -> dict:
        """
        Activate a strategy.

        Creates a git worktree at the given revision, writes a config file,
        and spawns a subprocess running runner.py.

        Returns a dict with status and details.
        """
        if strategy_name in self._runners:
            return {"error": f"Strategy '{strategy_name}' is already running"}

        if len(self._runners) >= MAX_CONCURRENT_STRATEGIES:
            return {
                "error": f"Maximum concurrent strategies ({MAX_CONCURRENT_STRATEGIES}) reached. "
                         "Deactivate a running strategy first."
            }

        # Build worktree
        from dexorder.tools.python_tools import get_category_manager, sanitize_name
        mgr = get_category_manager(self.data_dir)

        safe_name = sanitize_name(strategy_name)
        impl_path = self.data_dir / "src" / "strategy" / safe_name / "implementation.py"
        if not impl_path.exists():
            return {"error": f"Strategy '{strategy_name}' not found at {impl_path}"}

        try:
            short_hash = mgr.git.head_short_hash() if git_revision == "HEAD" else git_revision[:7]
            worktree_name = f"{safe_name}_{short_hash}"
            worktree_path = self.worktrees_dir / worktree_name

            if not worktree_path.exists():
                actual_hash = mgr.git.create_worktree(worktree_path, git_revision)
            else:
                actual_hash = short_hash
        except Exception as e:
            return {"error": f"Failed to create git worktree: {e}"}

        worktree_impl = worktree_path / "src" / "strategy" / safe_name / "implementation.py"
        if not worktree_impl.exists():
            # Fall back to live impl (worktree may not include subdirs on first use)
            worktree_impl = impl_path

        # Feed configs as list of [ticker, period_seconds]
        feed_configs = [[f.get("symbol", ""), int(f.get("period_seconds", 3600))] for f in feeds]

        # Write runner config to a temp file under DATA_DIR
        runner_config = {
            "strategy_name": strategy_name,
            "impl_path": str(worktree_impl),
            "feed_configs": feed_configs,
            "allocation": allocation,
            "ipc_endpoint": "ipc:///tmp/dexorder-strategy-events.sock",
            "data_dir": str(self.data_dir),
            "poll_interval": DEFAULT_POLL_INTERVAL,
        }
        config_file = self.configs_dir / f"{safe_name}.json"
        config_file.write_text(json.dumps(runner_config, indent=2))

        # Launch strategy in a daemon thread
        try:
            from dexorder.strategy.runner import run_thread
            stop_event = threading.Event()
            thread = threading.Thread(
                target=run_thread,
                args=(runner_config, stop_event),
                daemon=True,
                name=f"strategy-{safe_name}",
            )
            thread.start()
        except Exception as e:
            return {"error": f"Failed to start strategy thread: {e}"}

        self._runners[strategy_name] = (thread, stop_event)

        # Register as lifecycle trigger
        if self._lifecycle:
            self._lifecycle.add_trigger(f"strategy:{strategy_name}")

        # Notify event bridge
        if self._bridge:
            self._bridge.notify_strategy_started(strategy_name)

        # Persist to DB
        if self._db:
            await self._db.upsert_strategy(
                name=strategy_name,
                status="running",
                allocation=allocation,
                paper=paper,
                feeds=feeds,
                git_rev=actual_hash,
                worktree_path=str(worktree_path),
                config=runner_config,
            )

        log.info("Strategy '%s' activated (thread=%d, rev=%s)", strategy_name, thread.ident, actual_hash)
        return {
            "status": "activated",
            "strategy_name": strategy_name,
            "paper": paper,
            "allocation": allocation,
            "git_revision": actual_hash,
            "thread_id": thread.ident,
        }

    async def deactivate(self, strategy_name: str) -> dict:
        """Stop a running strategy and clean up its worktree."""
        entry = self._runners.pop(strategy_name, None)
        if entry is None:
            return {"error": f"Strategy '{strategy_name}' is not running"}

        thread, stop_event = entry

        # Signal the runner to stop and wait for the thread to exit
        stop_event.set()
        await asyncio.get_event_loop().run_in_executor(
            None, lambda: thread.join(timeout=15)
        )
        if thread.is_alive():
            log.warning("Strategy '%s' thread did not exit within timeout", strategy_name)

        # Remove lifecycle trigger
        if self._lifecycle:
            self._lifecycle.remove_trigger(f"strategy:{strategy_name}")

        # Notify bridge
        if self._bridge:
            self._bridge.notify_strategy_stopped(strategy_name)

        # Get final PnL from DB
        final_pnl = 0.0
        if self._db:
            state = await self._db.get_pnl_state(strategy_name)
            if state:
                final_pnl = state.get("realized_pnl", 0.0)
            await self._db.update_strategy_status(strategy_name, "stopped")

        # Clean up worktree
        await self._cleanup_worktree(strategy_name)

        log.info("Strategy '%s' deactivated, final_pnl=%.4f", strategy_name, final_pnl)
        return {
            "status": "deactivated",
            "strategy_name": strategy_name,
            "final_pnl": final_pnl,
        }

    async def mark_crashed(self, strategy_name: str) -> None:
        """Mark a strategy as crashed (called by heartbeat monitor)."""
        self._runners.pop(strategy_name, None)
        if self._lifecycle:
            self._lifecycle.remove_trigger(f"strategy:{strategy_name}")
        if self._db:
            await self._db.update_strategy_status(strategy_name, "error")
        log.error("Strategy '%s' marked as crashed (heartbeat timeout)", strategy_name)

    async def update_pnl(self, strategy_name: str, payload: dict) -> None:
        """Called by event bridge when a PNL_UPDATE event arrives."""
        if self._db:
            await self._db.update_pnl_state(
                name=strategy_name,
                realized_pnl=payload.get("realized_pnl", 0.0),
                unrealized_pnl=payload.get("unrealized_pnl", 0.0),
                trade_count=payload.get("trade_count", 0),
            )

    # ------------------------------------------------------------------
    # Listing
    # ------------------------------------------------------------------

    async def list_active(self) -> list[dict]:
        """Return currently running strategies with PnL state."""
        if self._db is None:
            return []
        strategies = await self._db.get_running_strategies()
        result = []
        for s in strategies:
            name = s["name"]
            state = await self._db.get_pnl_state(name)
            entry = {
                "strategy_name": name,
                "status": s.get("status", "unknown"),
                "paper": bool(s.get("paper", 1)),
                "allocation": s.get("allocation", 0),
                "git_revision": s.get("git_rev"),
                "started_at": s.get("started_at"),
                "feeds": json.loads(s.get("feeds_json") or "[]"),
                "realized_pnl": state.get("realized_pnl", 0.0) if state else 0.0,
                "unrealized_pnl": state.get("unrealized_pnl", 0.0) if state else 0.0,
                "trade_count": state.get("trade_count", 0) if state else 0,
            }
            result.append(entry)
        return result

    # ------------------------------------------------------------------
    # Shutdown
    # ------------------------------------------------------------------

    async def shutdown(self) -> None:
        """Stop all running strategies on container shutdown."""
        names = list(self._runners.keys())
        for name in names:
            await self.deactivate(name)

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    async def _cleanup_worktree(self, strategy_name: str) -> None:
        if self._db is None:
            return
        try:
            row = await self._db.get_strategy(strategy_name)
            wt = row.get("worktree_path") if row else None
            if wt:
                from dexorder.tools.python_tools import get_category_manager
                mgr = get_category_manager(self.data_dir)
                mgr.git.remove_worktree(Path(wt))
        except Exception as e:
            log.warning("Worktree cleanup failed for '%s': %s", strategy_name, e)


# Singleton
_lifecycle_manager: Optional[StrategyLifecycleManager] = None


def get_strategy_lifecycle(data_dir: Optional[Path] = None) -> StrategyLifecycleManager:
    global _lifecycle_manager
    if _lifecycle_manager is None:
        if data_dir is None:
            import os
            data_dir = Path(os.environ.get("DATA_DIR", "/app/data"))
        _lifecycle_manager = StrategyLifecycleManager(data_dir)
    return _lifecycle_manager
