"""
=========================================
CV6 AI Trading OS
Angel One Broker
Production Version
=========================================
"""

from __future__ import annotations

import socket
import pyotp
import requests as _req
from typing import Any

from SmartApi import SmartConnect
from loguru import logger

from app.broker.base_broker import BaseBroker
from app.config.settings import settings

_ANGEL_BASE = "https://apiconnect.angelone.in/rest/secure/angelbroking"
_SCRIP_URL  = "https://margincalculator.angelbroking.com/OpenAPI_File/files/OpenAPIScripMaster.json"


class AngelOneBroker(BaseBroker):

    # Class-level scrip master cache — shared across instances so it only
    # downloads once per process (re-downloaded on each fresh connect).
    _scrip_master: dict = {}   # (symbol, exch_seg) -> token

    def __init__(self):

        self.smart_api: SmartConnect | None = None

        self.connected = False

        self.jwt_token = ""
        self.refresh_token = ""
        self.feed_token = ""
        self.local_ip  = "127.0.0.1"
        self.public_ip = "127.0.0.1"

        self.api_key = settings.ANGEL_API_KEY
        self.client_id = settings.ANGEL_CLIENT_ID
        self.pin = settings.ANGEL_PIN
        self.totp_secret = settings.ANGEL_TOTP_SECRET

    # ── Helpers ────────────────────────────────────────────────────────

    def _totp(self) -> str:
        return pyotp.TOTP(self.totp_secret).now()

    def _detect_local_ip(self) -> str:
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except Exception:
            return "127.0.0.1"

    def _detect_public_ip(self) -> str:
        for url in ("https://api.ipify.org?format=json", "https://ifconfig.me/all.json"):
            try:
                r = _req.get(url, timeout=5)
                d = r.json()
                ip = d.get("ip") or d.get("ip_addr")
                if ip:
                    return ip
            except Exception:
                continue
        return "127.0.0.1"

    def _load_scrip_master(self) -> None:
        """Download Angel One scrip master (all symbols + tokens). Takes ~1-2 sec."""
        try:
            resp = _req.get(_SCRIP_URL, timeout=30)
            scrips = resp.json()
            AngelOneBroker._scrip_master = {
                (s["symbol"], s["exch_seg"]): s["token"]
                for s in scrips
                if "symbol" in s and "token" in s and "exch_seg" in s
            }
            logger.info(f"[AngelOne] Scrip master loaded: {len(AngelOneBroker._scrip_master):,} symbols")
        except Exception as e:
            logger.warning(f"[AngelOne] Scrip master load failed: {e}")
            AngelOneBroker._scrip_master = {}

    def _get_symbol_token(self, symbol: str, exchange: str):
        """Return (actual_symbol, token) from scrip master. Falls back to prefix search for futures."""
        sm = AngelOneBroker._scrip_master

        # 1. Exact match
        for try_sym in [symbol, symbol + "-EQ"]:
            tok = sm.get((try_sym, exchange))
            if tok:
                return try_sym, tok

        # 2. For MCX/NFO: prefix search — e.g. "GOLDPETAL" → "GOLDPETAL26JULFUT"
        if exchange in ("MCX", "NFO", "CDS", "BFO"):
            candidates = [
                (sym, exch) for (sym, exch) in sm.keys()
                if exch == exchange and sym.startswith(symbol)
            ]
            if candidates:
                # Sort alphabetically — AngelOne's DDMMMYY format sorts by expiry date
                nearest = sorted(candidates)[0]
                logger.info(f"[AngelOne] Prefix match: {symbol} → {nearest[0]} (token {sm[nearest]})")
                return nearest[0], sm[nearest]

        # 3. NSE equity fallback
        if exchange == "NSE":
            tok = sm.get((symbol + "-EQ", "NSE"))
            if tok:
                return symbol + "-EQ", tok

        return symbol, "0"

    def _call(self, method: str, endpoint: str, payload=None, _retried: bool = False) -> dict:
        """Direct HTTP call to Angel One REST API — more reliable than SmartAPI SDK.
        TIER-2 (2A): on a session-expiry response, re-login once and retry."""
        headers = {
            "Content-Type":     "application/json",
            "Accept":           "application/json",
            "X-PrivateKey":     self.api_key,
            "X-UserType":       "USER",
            "X-SourceID":       "WEB",
            "X-ClientLocalIP":  self.local_ip,
            "X-ClientPublicIP": self.public_ip,
            "X-MACAddress":     "00:00:00:00:00:00",
            "Authorization":    self.jwt_token,
        }
        url = f"{_ANGEL_BASE}/{endpoint}"
        try:
            if method == "POST":
                r = _req.post(url, headers=headers, json=payload, timeout=15)
            else:
                r = _req.get(url, headers=headers, timeout=15)
            status_code = r.status_code
            resp = r.json()
        except Exception as e:
            return {"status": False, "message": str(e)}

        if not _retried and self._is_session_expired(resp, status_code):
            logger.warning(f"[AngelOne] Session expired on {endpoint} — attempting re-login")
            if self._relogin():
                return self._call(method, endpoint, payload, _retried=True)
        return resp

    # ── TIER-2 (2A): session-expiry detection + auto re-login ───────────────
    _EXPIRY_CODES = ("AG8001", "AG8002", "AG8003", "AB1010")
    _EXPIRY_MARKERS = ("invalid token", "session expired", "token expired",
                       "invalid session", "unauthorized", "expired")

    def _is_session_expired(self, resp, status_code: int = 0) -> bool:
        if status_code in (401, 403):
            return True
        if not isinstance(resp, dict):
            return False
        code = str(resp.get("errorcode") or resp.get("errorCode") or "")
        if code in self._EXPIRY_CODES:
            return True
        msg = str(resp.get("message") or "").lower()
        return any(m in msg for m in self._EXPIRY_MARKERS)

    def _relogin(self) -> bool:
        """Force a fresh session (used on detected expiry). Returns success."""
        try:
            self.connected = False
            result = self._login()
            ok = bool(result.get("success"))
            if ok:
                logger.info("[AngelOne] Re-login successful — session refreshed")
            else:
                logger.error(f"[AngelOne] Re-login FAILED: {result.get('message')}")
            return ok
        except Exception as e:
            logger.error(f"[AngelOne] Re-login exception: {e}")
            return False

    def validate_session(self) -> bool:
        """Lightweight proactive check — call periodically. Returns True if the
        session is (or was made) valid; re-logins on failure."""
        if not self.connected or self.smart_api is None:
            return self._relogin()
        try:
            prof = self.smart_api.getProfile(self.refresh_token)
            if self._is_session_expired(prof):
                return self._relogin()
            return True
        except Exception:
            return self._relogin()

    def _sdk_call(self, fn_name: str, *args, **kwargs):
        """Call a SmartAPI SDK method with one expiry-triggered re-login+retry."""
        def _invoke():
            try:
                fn = getattr(self.smart_api, fn_name)
                return fn(*args, **kwargs)
            except Exception as e:
                return {"status": False, "message": str(e)}
        resp = _invoke()
        if self._is_session_expired(resp):
            logger.warning(f"[AngelOne] Session expired on {fn_name} — re-login")
            if self._relogin():
                resp = _invoke()
        return resp

    def is_connected(self) -> bool:
        return self.connected

    def connect(self):
        if self.connected:
            return {"success": True, "message": "Already Connected"}
        return self._login()

    def _login(self) -> dict:
        """Perform a fresh Angel One session login. Shared by connect() and
        the TIER-2 re-login path so expiry recovery reuses the exact flow."""
        try:
            self.smart_api = SmartConnect(
                api_key=self.api_key
            )

            session = self.smart_api.generateSession(
                self.client_id,
                self.pin,
                self._totp()
            )

            if not session.get("status"):

                return {
                    "success": False,
                    "message": session.get(
                        "message",
                        "Login Failed"
                    )
                }

            data = session["data"]

            self.jwt_token = data["jwtToken"]
            self.refresh_token = data["refreshToken"]

            self.feed_token = self.smart_api.getfeedToken()

            # Detect real IPs (needed for Angel One REST API headers)
            self.local_ip  = self._detect_local_ip()
            self.public_ip = self._detect_public_ip()
            logger.info(f"[AngelOne] IPs — local={self.local_ip} public={self.public_ip}")

            self.connected = True

            # Load scrip master in background — gives us symbol tokens for all instruments
            try:
                self._load_scrip_master()
            except Exception as e:
                logger.warning(f"[AngelOne] Scrip master skipped: {e}")

            return {
                "success": True,
                "broker": "angelone",
                "client_id": self.client_id,
                "jwt_token": self.jwt_token,
                "feed_token": self.feed_token,
                "scrip_master_symbols": len(AngelOneBroker._scrip_master),
            }

        except Exception as e:

            self.connected = False

            return {
                "success": False,
                "message": str(e)
            }

    def disconnect(self):

        try:

            self.connected = False

            self.smart_api = None

            self.jwt_token = ""
            self.refresh_token = ""
            self.feed_token = ""

            return {
                "success": True
            }

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def profile(self):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            profile = self.smart_api.getProfile(
                self.refresh_token
            )

            return profile

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def funds(self):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self._sdk_call("rmsLimit")

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }
    def holdings(self):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self._sdk_call("holding")

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def positions(self):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self._sdk_call("position")

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def orders(self):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self._sdk_call("orderBook")

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def place_order(self, order: dict[str, Any]):
        """
        Place an order via Angel One REST API (direct HTTP, bypasses SmartAPI SDK).
        Accepts the same dict that the execution layer builds:
          tradingsymbol, exchange, transactiontype, ordertype, producttype,
          quantity, price, triggerprice, variety, duration
        """
        if not self.connected:
            return {"success": False, "message": "Broker not connected"}

        try:
            symbol   = order.get("tradingsymbol") or order.get("symbol", "")
            exchange = order.get("exchange", "NSE")

            # ── Auto-resolve symbol token from scrip master ──────────
            actual_symbol, token = self._get_symbol_token(symbol, exchange)
            if token == "0":
                logger.warning(
                    f"[AngelOne] Token=0 for {symbol}/{exchange}. "
                    f"Scrip master has {len(AngelOneBroker._scrip_master):,} symbols. "
                    "Order may be rejected."
                )

            # ── Map producttype ──────────────────────────────────────
            product_map = {
                "INTRADAY":    "INTRADAY",
                "MIS":         "INTRADAY",
                "DELIVERY":    "DELIVERY",
                "CNC":         "DELIVERY",
                "CARRYFORWARD":"CARRYFORWARD",
                "NRML":        "CARRYFORWARD",
                "MARGIN":      "MARGIN",
            }
            product = product_map.get(
                str(order.get("producttype") or order.get("product", "INTRADAY")).upper(),
                "INTRADAY"
            )

            # ── Map ordertype ────────────────────────────────────────
            order_type_map = {
                "MARKET":     "MARKET",
                "LIMIT":      "LIMIT",
                "SL":         "STOPLOSS_LIMIT",
                "SL-M":       "STOPLOSS_MARKET",
                "STOPLOSS_LIMIT":   "STOPLOSS_LIMIT",
                "STOPLOSS_MARKET":  "STOPLOSS_MARKET",
            }
            order_type = order_type_map.get(
                str(order.get("ordertype") or order.get("order_type", "MARKET")).upper(),
                "MARKET"
            )

            payload = {
                "variety":         str(order.get("variety", "NORMAL")),
                "tradingsymbol":   actual_symbol,
                "symboltoken":     str(token),
                "transactiontype": str(order.get("transactiontype") or order.get("transaction_type", "BUY")).upper(),
                "exchange":        exchange,
                "ordertype":       order_type,
                "producttype":     product,
                "duration":        str(order.get("duration", "DAY")),
                "price":           str(float(order.get("price") or 0)),
                "squareoff":       "0",
                "stoploss":        "0",
                "quantity":        str(int(order.get("quantity", 1))),
                "triggerprice":    str(float(order.get("triggerprice") or order.get("trigger_price") or 0)),
            }

            logger.info(f"[AngelOne] Placing order: {payload}")
            resp = self._call("POST", "order/v1/placeOrder", payload)
            logger.info(f"[AngelOne] placeOrder response: {resp}")

            if not resp.get("status"):
                msg = resp.get("message") or str(resp)
                err = resp.get("errorcode", "")
                # AG7002 = "not a registered IP" — public IP not whitelisted on Angel One portal
                if err == "AG7002" or "registered" in msg.lower() or "not a valid ip" in msg.lower():
                    public_ip = getattr(self, "public_ip", "unknown")
                    return {
                        "success": False,
                        "message": (
                            f"Angel One IP whitelist error (AG7002): your public IP {public_ip} is not registered. "
                            f"Go to https://smartapi.angelbroking.com → My Profile → IP Whitelist → add {public_ip}. "
                            f"Then reconnect Angel One."
                        ),
                        "errorcode": err,
                        "action_required": f"Whitelist IP {public_ip} at https://smartapi.angelbroking.com",
                    }
                return {"success": False, "message": f"Angel One rejected: {msg} (errorcode={err})"}

            order_id = (resp.get("data") or {}).get("orderid", "")
            if not order_id:
                return {
                    "success": False,
                    "message": (
                        f"Angel One returned status=true but NO order ID. "
                        f"Possible IP whitelist or risk-engine rejection. "
                        f"public_ip={self.public_ip} | resp={resp}"
                    )
                }

            logger.info(f"[AngelOne] Order placed successfully: {order_id}")
            return {"success": True, "order_id": str(order_id)}

        except Exception as e:
            logger.error(f"[AngelOne] place_order exception: {e}")
            return {"success": False, "message": str(e)}

    def modify_order(
        self,
        order_id: str,
        order: dict[str, Any]
    ):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            payload = dict(order)
            payload["orderid"] = order_id

            return self.smart_api.modifyOrder(payload)

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def cancel_order(self, order_id: str):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self.smart_api.cancelOrder(order_id)

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def quotes(self, symbol: dict[str, Any]):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self.smart_api.ltpData(
                symbol["exchange"],
                symbol["tradingsymbol"],
                symbol["symboltoken"]
            )

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def historical_data(self, params: dict[str, Any]):

        if not self.connected:

            return {
                "success": False,
                "message": "Broker not connected"
            }

        try:

            return self._sdk_call("getCandleData", params)

        except Exception as e:

            return {
                "success": False,
                "message": str(e)
            }

    def live_data(self, symbol: dict[str, Any]):

        return self.quotes(symbol)