import os
import stat
import sys
from typing import Optional

from dotenv import load_dotenv

# Load .env exactly once, relative to this file so it works under Passenger with a different CWD.
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))


def _load_or_create_secret_key() -> str:
    """Load Flask secret key from env or a persisted project-local file."""
    env_key = os.getenv("FLASK_SECRET_KEY", "").strip()
    if env_key:
        return env_key

    secret_file = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), ".flask_secret"
    )
    if os.path.exists(secret_file):
        with open(secret_file, "r") as f:
            key = f.read().strip()
        if key:
            return key

    key = os.urandom(32).hex()
    try:
        fd = os.open(secret_file, os.O_WRONLY | os.O_CREAT, 0o600)
        try:
            os.write(fd, key.encode("utf-8"))
        finally:
            os.close(fd)
    except OSError:
        # e.g. read-only filesystem in some hosting setups
        pass
    return key


# ── Telegram API Configuration ──────────────────────────────────────────────
TG_API_ID: Optional[int] = int(os.getenv("TG_API_ID", "0")) or None
TG_API_HASH: Optional[str] = os.getenv("TG_API_HASH")
TG_PHONE: str = os.getenv("TG_PHONE", "")
TG_PASSWORD: str = os.getenv("TG_PASSWORD", "")

# ── Web Dashboard Configuration ─────────────────────────────────────────────
WEB_PIN: str = os.getenv("WEB_PIN", "")
FLASK_SECRET_KEY: str = _load_or_create_secret_key()
PORT: int = int(os.environ.get("PORT", "8301"))

# Validate WEB_PIN format (digits only, min 4 chars) when configured.
if WEB_PIN and (len(WEB_PIN) < 4 or not WEB_PIN.isdigit()):
    raise ValueError("WEB_PIN must be at least 4 digits")

# ── Message map retention ───────────────────────────────────────────────────
MAP_RETENTION_DAYS: int = int(os.getenv("MAP_RETENTION_DAYS", "30"))

# ── Dialogs cache ───────────────────────────────────────────────────────────
DIALOGS_CACHE_FILE: str = os.getenv(
    "DIALOGS_CACHE_FILE",
    os.path.join(os.path.dirname(os.path.abspath(__file__)), ".dialogs_cache.json"),
)
DIALOGS_CACHE_TTL_SECONDS: int = int(os.getenv("DIALOGS_CACHE_TTL_SECONDS", "86400"))

# ── Signal price-augmentation bot ───────────────────────────────────────────
# Used to send price-augmented snapshots when a channel pair has price_augment=true.
# Also used to forward messages when a pair has forward_via_bot=true.
SIGNAL_BOT_TOKEN: str = os.getenv("SIGNAL_BOT_TOKEN", "")
SIGNAL_ADMIN_CHAT_ID: int = int(os.getenv("SIGNAL_ADMIN_CHAT_ID", "0"))

# Optional outbound webhook called after a price-augmented signal is processed.
# Allows downstream systems to receive signals without touching Telegram.
SIGNAL_WEBHOOK_URL: str = os.getenv("SIGNAL_WEBHOOK_URL", "")
SIGNAL_WEBHOOK_SECRET: str = os.getenv("SIGNAL_WEBHOOK_SECRET", "")

# ── Optional OpenAI-compatible LLM signal classification ──────────────────
# Off by default. These env vars only *seed* the DB-backed config on first use;
# the web dashboard (LLM tab) is the source of truth afterwards.
# base_url must point at a provider exposing the OpenAI /chat/completions API
# (e.g. https://api.cerebras.ai/v1, https://api.openai.com/v1,
# https://openrouter.ai/api/v1, https://api.groq.com/openai/v1,
# http://localhost:11434/v1 for Ollama). Get a free key at cloud.cerebras.ai
LLM_ENABLED: bool = os.getenv("LLM_ENABLED", "").lower() in ("1", "true", "yes")
LLM_PROVIDER: str = os.getenv("LLM_PROVIDER", "openai-compatible")
LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "https://api.cerebras.ai/v1")
LLM_MODEL: str = os.getenv("LLM_MODEL", "llama3.1-8b")
LLM_API_KEY: str = os.getenv("LLM_API_KEY", "")
LLM_MIN_CONFIDENCE: float = float(os.getenv("LLM_MIN_CONFIDENCE", "0.80"))
LLM_TIMEOUT: float = float(os.getenv("LLM_TIMEOUT", "12.0"))
LLM_MAX_TOKENS: int = int(os.getenv("LLM_MAX_TOKENS", "120"))
LLM_WEBHOOK_URL: str = os.getenv("LLM_WEBHOOK_URL", "")
LLM_WEBHOOK_SECRET: str = os.getenv("LLM_WEBHOOK_SECRET", "")

# ── cTrader OAuth App credentials ───────────────────────────────────────────
# Used to build the consent URL for /callback and for protobuf app auth.
CTRADER_CLIENT_ID: str = os.getenv("CTRADER_CLIENT_ID", "")
CTRADER_CLIENT_SECRET: str = os.getenv("CTRADER_CLIENT_SECRET", "")
CTRADER_REDIRECT_URI: str = os.getenv(
    "CTRADER_REDIRECT_URI", "https://quill.nx.kg/f/callback"
)

# ── cTrader Open API runtime credentials ────────────────────────────────────
# At runtime the spot client needs an access_token.  It is injected by the
# caller (core.py) from the local DB vault or from an external source.
# CTRADER_ACCESS_TOKEN is still supported as a manual override.
CTRADER_ACCESS_TOKEN: str = os.getenv("CTRADER_ACCESS_TOKEN", "")
CTRADER_ACCOUNT_ID: int = int(os.getenv("CTRADER_ACCOUNT_ID", "0"))
CTRADER_USE_LIVE: bool = os.getenv("CTRADER_USE_LIVE", "").lower() in (
    "1",
    "true",
    "yes",
)
CTRADER_SYMBOL: str = os.getenv("CTRADER_SYMBOL", "XAUUSD")
CTRADER_SYMBOL_ID: int = int(os.getenv("CTRADER_SYMBOL_ID", "0"))
CTRADER_PRICE_TIMEOUT: float = float(os.getenv("CTRADER_PRICE_TIMEOUT", "8.0"))

# ── cTrader Token Vault (symmetric encryption) ──────────────────────────────
# A single shared secret is used to encrypt/decrypt cTrader tokens.
# Keep this secret out of version control and rotate only if compromised
# (rotating invalidates tokens already stored in the database).
CTRADER_TOKEN_SECRET: str = os.getenv("CTRADER_TOKEN_SECRET", "")

# ── MariaDB Configuration ───────────────────────────────────────────────────
MYSQL_HOST: str = os.getenv("MYSQL_HOST", "localhost")
MYSQL_USER: str = os.getenv("MYSQL_USER", "")
MYSQL_PASSWORD: str = os.getenv("MYSQL_PASSWORD", "")
MYSQL_DB: str = os.getenv("MYSQL_DB", "")

# ── Multi-account limits ────────────────────────────────────────────────────
MAX_TG_ACCOUNTS: int = int(os.getenv("MAX_TG_ACCOUNTS", "3"))
QR_LOGIN_TIMEOUT: int = int(os.getenv("QR_LOGIN_TIMEOUT", "120"))

# ── Core lifecycle ───────────────────────────────────────────────────────────
# When False, the Telegram core is NOT auto-started on the first HTTP request.
# Use this when you want to start/stop the whole app from the cPanel Python
# Selector and only connect to Telegram when explicitly enabled (e.g. by setting
# this env var in the cPanel UI, or by calling the manual start endpoint).
AUTO_START_CORE: bool = os.getenv("AUTO_START_CORE", "").lower() in (
    "1",
    "true",
    "yes",
)

# ── DB-backed overrides ─────────────────────────────────────────────────────
# These override the env-var defaults at runtime via patch_from_db().
# The type map tells patch_from_db how to convert strings back.
_db_type_map = {
    "bool": lambda v: v.lower() in ("1", "true", "yes"),
    "int": int,
    "float": float,
    "str": str,
}

_db_overrides = {
    # (attr_name, db_key, type)
    "TG_API_ID": ("tg_api_id", "int"),
    "TG_PHONE": ("tg_phone", "str"),
    "TG_PASSWORD": ("tg_password", "str"),
    "WEB_PIN": ("web_pin", "str"),
    "SIGNAL_ADMIN_CHAT_ID": ("signal_admin_chat_id", "int"),
    "SIGNAL_WEBHOOK_URL": ("signal_webhook_url", "str"),
    "SIGNAL_WEBHOOK_SECRET": ("signal_webhook_secret", "str"),
    "LLM_ENABLED": ("llm_enabled", "bool"),
    "LLM_PROVIDER": ("llm_provider", "str"),
    "LLM_BASE_URL": ("llm_base_url", "str"),
    "LLM_MODEL": ("llm_model", "str"),
    "LLM_MIN_CONFIDENCE": ("llm_min_confidence", "float"),
    "LLM_TIMEOUT": ("llm_timeout", "float"),
    "LLM_MAX_TOKENS": ("llm_max_tokens", "int"),
    "LLM_WEBHOOK_URL": ("llm_webhook_url", "str"),
    "CTRADER_CLIENT_ID": ("ctrader_client_id", "str"),
    "CTRADER_CLIENT_SECRET": ("ctrader_client_secret", "str"),
    "CTRADER_REDIRECT_URI": ("ctrader_redirect_uri", "str"),
    "CTRADER_ACCESS_TOKEN": ("ctrader_access_token", "str"),
    "CTRADER_ACCOUNT_ID": ("ctrader_account_id", "int"),
    "CTRADER_USE_LIVE": ("ctrader_use_live", "bool"),
    "CTRADER_SYMBOL": ("ctrader_symbol", "str"),
    "CTRADER_SYMBOL_ID": ("ctrader_symbol_id", "int"),
    "CTRADER_PRICE_TIMEOUT": ("ctrader_price_timeout", "float"),
    "CTRADER_TOKEN_SECRET": ("ctrader_token_secret", "str"),
    "MAP_RETENTION_DAYS": ("map_retention_days", "int"),
    "MAX_TG_ACCOUNTS": ("max_tg_accounts", "int"),
    "QR_LOGIN_TIMEOUT": ("qr_login_timeout", "int"),
    "PORT": ("port", "int"),
}

# Secret keys (Fernet-encrypted in DB)
_db_secret_overrides = {
    "TG_API_HASH": "tg_api_hash",
    "SIGNAL_BOT_TOKEN": "signal_bot_token",
    "CTRADER_CLIENT_SECRET": "ctrader_client_secret",
    "CTRADER_ACCESS_TOKEN": "ctrader_access_token",
    "CTRADER_TOKEN_SECRET": "ctrader_token_secret",
    "LLM_API_KEY": "llm_api_key",
    "LLM_WEBHOOK_SECRET": "llm_webhook_secret",
    "SIGNAL_WEBHOOK_SECRET": "signal_webhook_secret",
    "TG_PASSWORD": "tg_password",
}


def patch_from_db(db):
    """Override module-level config from DB config_vars table.

    Call once after DB init, before any request handling.
    """
    this = sys.modules[__name__]
    for attr, (db_key, type_name) in _db_overrides.items():
        val = db.get_config(db_key)
        if val is None:
            continue
        try:
            if type_name == "bool":
                setattr(this, attr, _db_type_map["bool"](val))
            elif type_name == "int":
                setattr(this, attr, int(val))
            elif type_name == "float":
                setattr(this, attr, float(val))
            else:
                setattr(this, attr, val)
        except (ValueError, TypeError):
            continue

    for attr, db_key in _db_secret_overrides.items():
        val = db.get_secret_config(db_key)
        if val is not None:
            setattr(this, attr, val)


def get_all_env(db):
    """Return all config values (env defaults, plus DB overrides) as a flat dict.

    Used by the settings UI to pre-fill form fields.
    """
    this = sys.modules[__name__]

    # Build a set of all attr→db_key mappings from both override maps
    result = {}

    for attr, (db_key, type_name) in _db_overrides.items():
        val = getattr(this, attr, None)
        if isinstance(val, bool):
            result[db_key] = "1" if val else "0"
        elif val is not None:
            result[db_key] = str(val)
        else:
            result[db_key] = ""

    # Secret overrides (encrypted in DB, decrypted at module level)
    for attr, db_key in _db_secret_overrides.items():
        val = getattr(this, attr, None)
        if val:
            if len(val) > 8:
                result[db_key] = val[:4] + "…" + val[-4:]
            else:
                result[db_key] = "••••••••"
        else:
            result[db_key] = ""

    return result
