# app/v1/services/gpt_engine.py
"""
GPT-specific configuration and helper functions for the ChatGPT-based
trading engine. This module is responsible **only** for talking to
OpenAI models and building prompts; it has no Zerodha or DB logic.
"""

import os
import json
import logging
from typing import Dict, Any

from fastapi import HTTPException
from app.v1.services.gpt_prompts import prepare_market_data_prompt

# OpenAI integration
try:
    from openai import OpenAI
    OPENAI_AVAILABLE = True
except ImportError:  # pragma: no cover - handled at runtime
    OpenAI = None  # type: ignore
    OPENAI_AVAILABLE = False

logger = logging.getLogger(__name__)

# OpenAI configuration
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
OPENAI_MODEL_FALLBACK = os.getenv("OPENAI_MODEL_FALLBACK", "gpt-4o-mini")
OPENAI_TEMPERATURE = float(os.getenv("OPENAI_TEMPERATURE", "0.1"))
MAX_TOKENS = int(os.getenv("MAX_TOKENS", "1000"))
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))


def _looks_like_model_error(exc: Exception) -> bool:
    msg = str(exc or "").lower()
    if not msg:
        return False
    return (
        "model" in msg
        and (
            "not found" in msg
            or "does not exist" in msg
            or "no such model" in msg
            or "model_not_found" in msg
            or "unsupported model" in msg
        )
    )


def _max_tokens_kwarg(model: str, max_tokens: int) -> Dict[str, Any]:
    """Return the correct completion-length kwarg for the chosen model.

    Some newer models (e.g., GPT-5.*) do not accept `max_tokens` and instead
    require `max_completion_tokens`.
    """

    m = (model or "").strip().lower()
    if m.startswith("gpt-5"):
        return {"max_completion_tokens": int(max_tokens)}
    return {"max_tokens": int(max_tokens)}


def validate_openai_setup() -> Dict[str, Any]:
    """Validate OpenAI configuration and basic connectivity.

    This is used by the `/health` endpoint to report whether GPT
    analysis is available.
    """
    if not OPENAI_AVAILABLE:
        return {"configured": False, "message": "OpenAI package not installed"}

    if not OPENAI_API_KEY:
        return {"configured": False, "message": "OPENAI_API_KEY not set"}

    try:
        client = OpenAI(api_key=OPENAI_API_KEY)
        # Lightweight ping to verify credentials/model
        client.chat.completions.create(
            model=OPENAI_MODEL,
            messages=[{"role": "user", "content": "Test"}],
            **_max_tokens_kwarg(OPENAI_MODEL, 10),
        )
        return {
            "configured": True,
            "message": f"OpenAI configured with model {OPENAI_MODEL}",
        }
    except Exception as e:  # pragma: no cover - runtime check
        return {"configured": False, "message": f"OpenAI test failed: {str(e)}"}


def get_openai_client() -> OpenAI:
    """Return an authenticated OpenAI client instance or raise HTTP error."""
    if not OPENAI_AVAILABLE:
        raise HTTPException(status_code=503, detail="OpenAI package not available")

    if not OPENAI_API_KEY:
        raise HTTPException(status_code=503, detail="OpenAI API key not configured")

    return OpenAI(api_key=OPENAI_API_KEY)


def call_chatgpt_analysis(prompt: str) -> Dict[str, Any]:
    """Call ChatGPT for structured JSON analysis with detailed logging."""
    try:
        logger.info(
            f"🤖 CHATGPT API CALL - Model: {OPENAI_MODEL}, Temp: {OPENAI_TEMPERATURE}"
        )
        client = get_openai_client()

        messages = [
            {
                "role": "system",
                "content": (
                    "You are an expert institutional intraday trader and risk manager. "
                    "You infer the best intraday decision purely from provided data. "
                    "Priority: capital protection, trade only when edge is clear, precision over frequency. "
                    "If no clear edge exists, you MUST choose HOLD. "
                    "Output ONLY valid JSON (no code blocks, no extra text)."
                ),
            },
            {"role": "user", "content": prompt},
        ]

        token_kwargs = _max_tokens_kwarg(OPENAI_MODEL, MAX_TOKENS)
        try:
            response = client.chat.completions.create(
                model=OPENAI_MODEL,
                messages=messages,
                temperature=OPENAI_TEMPERATURE,
                **token_kwargs,
                timeout=REQUEST_TIMEOUT,
            )
        except Exception as e:  # pragma: no cover - external API
            # Defensive retry for GPT-5.* incompatibility where a caller may still
            # end up sending `max_tokens`.
            msg = str(e)
            if "max_tokens" in msg and "max_completion_tokens" in msg:
                logger.warning(
                    "Retrying OpenAI call with max_completion_tokens due to parameter mismatch (model=%s)",
                    OPENAI_MODEL,
                )
                response = client.chat.completions.create(
                    model=OPENAI_MODEL,
                    messages=messages,
                    temperature=OPENAI_TEMPERATURE,
                    max_completion_tokens=int(MAX_TOKENS),
                    timeout=REQUEST_TIMEOUT,
                )
            elif OPENAI_MODEL_FALLBACK and OPENAI_MODEL_FALLBACK != OPENAI_MODEL and _looks_like_model_error(e):
                fb = OPENAI_MODEL_FALLBACK
                logger.warning(
                    "OpenAI model failed (%s). Retrying with fallback model=%s",
                    OPENAI_MODEL,
                    fb,
                )
                response = client.chat.completions.create(
                    model=fb,
                    messages=messages,
                    temperature=OPENAI_TEMPERATURE,
                    **_max_tokens_kwarg(fb, MAX_TOKENS),
                    timeout=REQUEST_TIMEOUT,
                )
            else:
                raise

        # Usage is best-effort (depends on model/client settings).
        try:
            usage = getattr(response, "usage", None)
            if usage:
                logger.info(
                    "📦 GPT usage | prompt=%s completion=%s total=%s",
                    getattr(usage, "prompt_tokens", None),
                    getattr(usage, "completion_tokens", None),
                    getattr(usage, "total_tokens", None),
                )
        except Exception:
            pass

        content = response.choices[0].message.content.strip()
        logger.info(f"📥 CHATGPT RAW RESPONSE (first 500 chars): {content[:500]}...")

        # Try to extract JSON from response
        try:
            start_idx = content.find("{")
            end_idx = content.rfind("}") + 1

            logger.info(f"🔍 JSON EXTRACTION - Start: {start_idx}, End: {end_idx}")

            if start_idx >= 0 and end_idx > start_idx:
                json_str = content[start_idx:end_idx]
                logger.info(f"📋 EXTRACTED JSON STRING: {json_str}")

                analysis = json.loads(json_str)
                logger.info("✅ JSON PARSED SUCCESSFULLY")
                logger.info(f"🎯 PARSED ANALYSIS KEYS: {list(analysis.keys())}")

                # Backward/forward compatible required fields
                if "decision" not in analysis:
                    analysis["decision"] = "HOLD"

                # New contract prefers `conviction`; older outputs used `confidence`.
                if "conviction" not in analysis and "confidence" in analysis:
                    analysis["conviction"] = analysis.get("confidence")
                if "confidence" not in analysis and "conviction" in analysis:
                    analysis["confidence"] = analysis.get("conviction")

                if "conviction" not in analysis:
                    analysis["conviction"] = "LOW"
                if "confidence" not in analysis:
                    analysis["confidence"] = analysis.get("conviction", "LOW")

                if "rationale" not in analysis:
                    analysis["rationale"] = ["No analysis provided"]

                # Enforce key HOLD invariants defensively
                try:
                    if str(analysis.get("decision") or "").upper() == "HOLD":
                        analysis["conviction"] = "LOW"
                        analysis["confidence"] = "LOW"
                        # Ensure score is < 40 for HOLD per prompt contract
                        score = analysis.get("score")
                        try:
                            score_f = float(score) if score is not None else 0.0
                        except Exception:
                            score_f = 0.0
                        analysis["score"] = int(min(39, max(0, int(score_f))))
                        # For HOLD, keep trade levels unset so UIs don't render 1.00/0.00.
                        # We normalize common "0" sentinel values to null/empty.
                        ez = analysis.get("entry_zone")
                        if isinstance(ez, dict):
                            low = ez.get("low") if "low" in ez else ez.get("lower")
                            high = ez.get("high") if "high" in ez else ez.get("upper")
                            try:
                                low_f = float(low) if low is not None else None
                                high_f = float(high) if high is not None else None
                            except Exception:
                                low_f, high_f = None, None
                            if (low_f is not None and low_f <= 0) or (high_f is not None and high_f <= 0):
                                analysis["entry_zone"] = None
                        elif ez in (0, 0.0):
                            analysis["entry_zone"] = None

                        for k in ("entry_price", "stop_loss", "risk_reward_ratio"):
                            try:
                                v = analysis.get(k)
                                if v is None:
                                    continue
                                if float(v) <= 0:
                                    analysis[k] = None
                            except Exception:
                                pass

                        t = analysis.get("targets")
                        if isinstance(t, list):
                            try:
                                cleaned = [float(x) for x in t if x is not None and float(x) > 0]
                            except Exception:
                                cleaned = []
                            analysis["targets"] = cleaned
                        elif t in (0, 0.0, None):
                            analysis["targets"] = []
                except Exception:
                    pass

                # Normalize optional numeric fields for downstream use
                # 1) decision_probability: 0-100, fallback from score/confidence
                prob = analysis.get("decision_probability")
                try:
                    if prob is not None:
                        prob = float(prob)
                    elif "score" in analysis and analysis.get("score") is not None:
                        prob = float(analysis["score"])
                    else:
                        # map HIGH/MEDIUM/LOW to a rough percentage
                        conf = str(analysis.get("confidence", analysis.get("conviction", "LOW"))).upper()
                        if conf == "HIGH":
                            prob = 80.0
                        elif conf == "MEDIUM":
                            prob = 60.0
                        else:
                            prob = 40.0
                except Exception:
                    prob = 50.0
                if prob is not None:
                    # clamp to [0,100]
                    prob = max(0.0, min(100.0, prob))
                    analysis["decision_probability"] = prob

                return analysis

            logger.error(f"❌ NO JSON FOUND in response. Content: {content}")
            raise ValueError("No JSON found in response")

        except json.JSONDecodeError as e:
            logger.error(f"❌ JSON DECODE ERROR: {e}")
            logger.error(f"🔍 FAILED JSON STRING: {content[:1000]}")
            return {
                "decision": "HOLD",
                "confidence": "LOW",
                "rationale": [
                    "Analysis failed - invalid JSON response format",
                    f"Error: {str(e)}",
                ],
                "error": "JSON parse error",
                "raw_response": content[:500],
            }

    except Exception as e:  # pragma: no cover - external API
        # Normalize common OpenAI error shapes so callers can make decisions.
        status_code = getattr(e, "status_code", None) or getattr(getattr(e, "response", None), "status_code", None)
        message = str(e)
        error_type = None

        try:
            body = getattr(e, "body", None) or getattr(getattr(e, "response", None), "json", lambda: None)()
            if isinstance(body, dict):
                err = body.get("error") or {}
                if isinstance(err, dict):
                    error_type = err.get("code") or err.get("type")
                    message = err.get("message") or message
        except Exception:
            pass

        if status_code == 429 and (error_type == "insufficient_quota" or "insufficient_quota" in message):
            logger.error("OpenAI quota exceeded (insufficient_quota)")
            return {
                "decision": "HOLD",
                "confidence": "LOW",
                "rationale": [
                    "GPT analysis temporarily unavailable (quota exceeded).",
                    "Using non-GPT fallback is recommended until billing is updated.",
                ],
                "error": message,
                "error_code": 429,
                "error_type": "insufficient_quota",
            }

        logger.exception("ChatGPT analysis failed")
        return {
            "decision": "HOLD",
            "confidence": "LOW",
            "rationale": [f"Analysis failed: {message}"],
            "error": message,
            "error_code": status_code,
            "error_type": error_type,
        }
