#!/usr/bin/env python3
"""Demand-Driven Innovation — automatic strategy generation to fill market gaps.

Analyzes the current strategy portfolio for coverage gaps (regime, direction,
timeframe) and produces a "demand signal" that triggers AI code generation
for the missing strategy type.  Generated strategies are submitted to the
review board for human approval before advancing.

Usage::

    from demand_driven_innovation import DemandDrivenInnovation

    ddi = DemandDrivenInnovation()
    gaps = ddi.analyze_gaps()
    if gaps["demand_signal"]:
        result = ddi.innovate()
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import config_manager

SRC_DIR = Path(__file__).resolve().parent
try:
    PROJECT = config_manager.get_project_dir()
except Exception:
    PROJECT = SRC_DIR.parent

# All possible regimes, directions, timeframes to check coverage
_ALL_REGIMES = ["STRONG_TREND", "WEAK_TREND", "RANGING", "TRANSITION"]
_REGIME_COVERAGE_KEYWORDS = {
    "STRONG_TREND": ("strong_trend", "breakout", "extreme_trend"),
    "WEAK_TREND": ("weak_trend", "trending", "trend"),
    "RANGING": ("ranging", "range", "consolidation"),
    "TRANSITION": ("transition", "uncertain", "drifting"),
}
_ALL_DIRECTIONS = ["BUY", "SELL"]
_ALL_TIMEFRAMES = ["M5", "M15", "H1"]


@dataclass
class InnovationConfig:
    """Controls for autonomous strategy generation."""

    max_strategies_total: int = 12
    max_innovations_per_day: int = 1
    min_gap_severity: int = 2
    cooldown_hours: int = 24
    auto_submit_to_review: bool = True
    tags: list[str] = field(default_factory=lambda: ["ai-generated", "demand-driven"])
    capabilities: list[str] = field(default_factory=lambda: ["intraday"])


class DemandDrivenInnovation:
    """Analyzes portfolio gaps and triggers AI strategy generation.

    Pipeline:
      1. Scan manifest files for current coverage.
      2. Cross-reference with PnL data to find performance gaps.
      3. Generate a demand signal describing the needed strategy.
      4. Feed demand signal to StrategyCodeGenerator.
      5. Submit generated code to StrategyReviewBoard.
    """

    def __init__(self, config: InnovationConfig | None = None):
        self._config = config or InnovationConfig()
        self._history: list[dict[str, Any]] = []
        self._load_history()

    # -- paths -----------------------------------------------------------

    def _history_path(self) -> Path:
        return config_manager.get_logs_dir() / "innovation_history.ndjson"

    # -- persistence -----------------------------------------------------

    def _load_history(self) -> None:
        path = self._history_path()
        if not path.exists():
            return
        try:
            for line in path.read_text(encoding="utf-8").strip().splitlines():
                if line.strip():
                    self._history.append(json.loads(line))
        except Exception:
            pass

    def _save_history(self, entry: dict[str, Any]) -> None:
        path = self._history_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        entry["logged_at"] = datetime.now(timezone.utc).isoformat()
        self._history.append(entry)
        # Keep last 100
        if len(self._history) > 100:
            self._history = self._history[-100:]
        with open(path, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")

    # -- gap analysis ----------------------------------------------------

    def analyze_gaps(self) -> dict[str, Any]:
        """Scan portfolio coverage and identify gaps.

        Returns a dict with ``coverage``, ``gaps``, ``demand_signal``
        (natural language description), and ``severity``.
        """
        portfolio = self._scan_portfolio()

        gaps: list[dict[str, str]] = []
        coverage = {
            "by_regime": {r: [] for r in _ALL_REGIMES},
            "by_direction": {d: [] for d in _ALL_DIRECTIONS},
            "by_timeframe": {t: [] for t in _ALL_TIMEFRAMES},
        }

        for sid, meta in portfolio.items():
            tags = meta.get("tags", [])
            capabilities = meta.get("capabilities", [])
            mode = meta.get("default_mode", "")
            trust = meta.get("trust_level", "sandbox")

            if trust in ("retired",):
                continue

            desc = meta.get("description", "").lower()
            name = meta.get("name", "").lower()

            # Regime coverage
            for regime in _ALL_REGIMES:
                keywords = _REGIME_COVERAGE_KEYWORDS.get(regime, (regime.lower(),))
                haystack = " ".join([desc, name, str(tags).lower()])
                if any(keyword in haystack for keyword in keywords):
                    coverage["by_regime"][regime].append(sid)

            # Direction coverage — infer from tags or name
            if "buy" in name or "buy" in desc or "long" in str(tags).lower():
                coverage["by_direction"]["BUY"].append(sid)
            if "sell" in name or "sell" in desc or "short" in str(tags).lower():
                coverage["by_direction"]["SELL"].append(sid)
            if "both" in str(tags).lower() or "reversal" in name:
                coverage["by_direction"]["BUY"].append(sid)
                coverage["by_direction"]["SELL"].append(sid)

            # Timeframe coverage
            for tf in _ALL_TIMEFRAMES:
                if tf in str(meta.get("supported_timeframes", [])):
                    coverage["by_timeframe"][tf].append(sid)

        # Identify missing regime coverage
        for regime in _ALL_REGIMES:
            if not coverage["by_regime"][regime]:
                gaps.append(
                    {
                        "type": "regime",
                        "dimension": regime,
                        "description": f"No strategy covers {regime} market regime",
                    }
                )

        # Identify missing direction coverage
        for direction in _ALL_DIRECTIONS:
            if not coverage["by_direction"][direction]:
                gaps.append(
                    {
                        "type": "direction",
                        "dimension": direction,
                        "description": f"No strategy specializes in {direction} direction",
                    }
                )

        # Check for performance vacuum: are ALL live strategies losing?
        performance_gap = self._check_performance_vacuum(portfolio)

        severity = len(gaps) + (2 if performance_gap else 0)

        demand_signal = self._build_demand_signal(gaps, performance_gap, portfolio)

        return {
            "analyzed_at": datetime.now(timezone.utc).isoformat(),
            "total_strategies": len(portfolio),
            "active_strategies": sum(
                1 for m in portfolio.values()
                if m.get("trust_level") not in ("retired", "sandbox")
            ),
            "coverage": {
                "by_regime": {
                    r: len(ids) for r, ids in coverage["by_regime"].items()
                },
                "by_direction": {
                    d: len(ids) for d, ids in coverage["by_direction"].items()
                },
            },
            "gaps": gaps,
            "performance_vacuum": performance_gap,
            "severity": severity,
            "demand_signal": demand_signal,
        }

    def _scan_portfolio(self) -> dict[str, dict[str, Any]]:
        strategies_dir = config_manager.get_project_dir() / "strategies"
        if not strategies_dir.is_dir():
            return {}

        portfolio: dict[str, dict[str, Any]] = {}
        for entry in sorted(strategies_dir.iterdir()):
            if not entry.name.endswith(".manifest.json"):
                continue
            try:
                manifest = json.loads(entry.read_text(encoding="utf-8"))
                portfolio[manifest["strategy_id"]] = manifest
            except Exception:
                continue
        return portfolio

    def _check_performance_vacuum(
        self, portfolio: dict[str, dict[str, Any]]
    ) -> bool:
        """Return True if all live-level strategies have negative total PnL."""
        from src.pnl_feedback_scorer import PnLFeedbackScorer

        scorer = PnLFeedbackScorer(min_trades_for_score=1)
        live_ids = [
            sid
            for sid, m in portfolio.items()
            if m.get("trust_level") in ("live_reduced", "live_full", "proven")
        ]
        if len(live_ids) < 2:
            return False

        all_negative = True
        for sid in live_ids:
            data = scorer.score_strategy(sid)
            if data.get("live_score", 0.0) > 0:
                all_negative = False
                break

        return all_negative

    def _build_demand_signal(
        self,
        gaps: list[dict[str, str]],
        performance_vacuum: bool,
        portfolio: dict[str, dict[str, Any]],
    ) -> str:
        """Build a natural language demand signal for the code generator."""
        parts: list[str] = []

        if gaps:
            for g in gaps:
                parts.append(g["description"])

        if performance_vacuum:
            parts.append(
                "All live strategies are currently losing money. "
                "Generate a strategy that uses a completely different approach "
                "than existing strategies, preferably counter-trend or "
                "mean-reversion on extreme RSI levels."
            )

        if not parts:
            # No gaps, but check if we have enough strategy diversity
            if len(portfolio) < 3:
                parts.append(
                    "Generate a diversified gold trading strategy that complements "
                    "the existing portfolio with a unique signal source."
                )

        return "; ".join(parts) if parts else ""

    # -- innovation pipeline ---------------------------------------------

    def innovate(self, demand_signal: str = "") -> dict[str, Any]:
        """Run the full innovation pipeline: analyze → generate → submit.

        If demand_signal is empty, runs analyze_gaps() first to produce one.
        Returns the result dict from code generation + review submission.
        """
        if not demand_signal:
            analysis = self.analyze_gaps()
            demand_signal = analysis.get("demand_signal", "")
            severity = analysis.get("severity", 0)
        else:
            severity = self._config.min_gap_severity

        if not demand_signal and severity < self._config.min_gap_severity:
            return {
                "innovated": False,
                "reason": "No demand signal and severity below threshold",
                "strategy_id": "",
                "file_path": "",
            }

        if not self._can_innovate():
            return {
                "innovated": False,
                "reason": "Cooldown active or max strategies reached",
                "strategy_id": "",
                "file_path": "",
            }

        strategy_id = self._generate_strategy_id(demand_signal)

        from src.strategy_code_generator import StrategyCodeGenerator

        gen = StrategyCodeGenerator()
        result = gen.generate(
            demand_signal,
            strategy_id=strategy_id,
            display_name=demand_signal[:60],
        )

        if not result["valid"]:
            self._save_history(
                {
                    "innovated": False,
                    "strategy_id": strategy_id,
                    "reason": "generation failed",
                    "errors": result.get("errors", []),
                }
            )
            return {
                "innovated": False,
                "strategy_id": strategy_id,
                "reason": "Code generation validation failed",
                "errors": result.get("errors", []),
            }

        # Submit to review board if configured
        review_submitted = False
        if self._config.auto_submit_to_review:
            try:
                from src.strategy_review_board import StrategyReviewBoard

                board = StrategyReviewBoard()
                board.submit(
                    strategy_id,
                    source="demand_driven",
                    code=result["code"],
                )
                review_submitted = True
            except Exception:
                pass

        entry = {
            "innovated": True,
            "strategy_id": strategy_id,
            "demand_signal": demand_signal,
            "file_path": result.get("file_path", ""),
            "manifest_path": result.get("manifest_path", ""),
            "review_submitted": review_submitted,
            "generated_at": datetime.now(timezone.utc).isoformat(),
        }
        self._save_history(entry)
        return entry

    # -- helpers ---------------------------------------------------------

    def _can_innovate(self) -> bool:
        """Check rate limits and portfolio size."""
        total = len(self._scan_portfolio())
        if total >= self._config.max_strategies_total:
            return False

        # Cooldown check
        if self._history:
            recent = [
                h
                for h in self._history
                if h.get("innovated") and h.get("generated_at")
            ]
            if recent:
                last = recent[-1]
                last_at = last.get("generated_at", "")
                try:
                    last_dt = datetime.fromisoformat(last_at.replace("Z", "+00:00"))
                    hours_since = (
                        datetime.now(timezone.utc) - last_dt
                    ).total_seconds() / 3600
                    if hours_since < self._config.cooldown_hours:
                        return False
                except ValueError:
                    pass

        # Check today's innovation count
        today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
        today_count = sum(
            1
            for h in self._history
            if h.get("innovated") and today in str(h.get("generated_at", ""))
        )
        if today_count >= self._config.max_innovations_per_day:
            return False

        return True

    def _generate_strategy_id(self, demand_signal: str) -> str:
        import re

        slug = re.sub(r"[^a-z0-9]+", "_", demand_signal.lower()).strip("_")[:30]
        suffix = datetime.now(timezone.utc).strftime("%m%d_%H%M")
        return f"ddi_{slug}_{suffix}" if slug else f"ddi_auto_{suffix}"

    # -- query -----------------------------------------------------------

    def get_history(self) -> list[dict[str, Any]]:
        return list(self._history)

    def get_innovation_count(self) -> int:
        return sum(1 for h in self._history if h.get("innovated"))
