"""
Application Configuration
"""
import os
from typing import Dict, Literal

from pydantic import AliasChoices, Field, model_validator
from pydantic_settings import BaseSettings

LlmMode = Literal["qwen_only", "openai_only", "auto"]


def get_initial_config() -> Dict[str, str]:
    """Read initial_config.txt (key = value per line) and return dict."""
    cfg = {}
    try:
        # Prefer repo-root file so uvicorn cwd does not hide keys (e.g. oci_call).
        repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
        candidates = [
            os.path.join(repo_root, "initial_config.txt"),
            os.path.join(os.getcwd(), "initial_config.txt"),
            os.path.join("initial_config.txt"),
        ]
        cfg_path = None
        for p in candidates:
            ap = os.path.abspath(p)
            if os.path.isfile(ap):
                cfg_path = ap
                break
        if cfg_path and os.path.isfile(cfg_path):
            with open(cfg_path, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    if "=" in line:
                        k, v = line.split("=", 1)
                        v = v.strip()
                        if len(v) >= 2 and (
                            (v[0] == '"' and v[-1] == '"')
                            or (v[0] == "'" and v[-1] == "'")
                        ):
                            v = v[1:-1]
                        v = v.rstrip().rstrip(";").strip()
                        cfg[k.strip()] = v
    except Exception as e:
        print(f"get_initial_config error: {e}")
    return cfg


def get_base_url() -> str:
    """Return API base URL from initial_config.txt (SCHEME + BASE_URL, e.g. http://localhost:8501)."""
    cfg = get_initial_config()
    base_url = (cfg.get("BASE_URL") or "").strip()
    scheme = (cfg.get("SCHEME") or "http").strip().lower()
    if not base_url:
        return "http://localhost:8000"
    if base_url.startswith("http://") or base_url.startswith("https://"):
        return base_url.rstrip("/")
    return f"{scheme}://{base_url}".rstrip("/")


class Settings(BaseSettings):
    """Application settings from environment variables"""

    # Application
    APP_NAME: str = "AskMeBackend"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = True

    # Database (SQLite for dev, PostgreSQL for production)
    DATABASE_URL: str = "sqlite:///./askme360_questions.db"

    # Security
    BYPASS_AUTH: bool = True  # Set to True to bypass token authentication for testing

    # Feature flags
    IS_ASKME_ACTIVE: bool = True
    IS_WEB_ACTIVE: bool = True
    # Talkback + multi_agent: reuse prior successful user_question_log rows (question_normalized) before routing.
    ENABLE_USER_QUESTION_LOG_CACHE: bool = True
    # After successful SQL execution, attach details.sql_query_summaries via LLM (disable to save latency).
    ENABLE_SQL_PLAIN_SUMMARY: bool = True
    # Document training: timestamp strip + fuzzy name dedup + replace-if-newer (see file_upload_check)
    CHECK_FILE: bool = False
    # is_qwen_llm / IS_QWEN_LLM: true = local Qwen (Ollama) only (no OpenAI fallback); false = OpenAI only;
    # auto (default) = Qwen 3.5 for summary, @web, and @askme (analysis/chart), then OpenAI fallback on failure.
    IS_QWEN_LLM: str = Field(
        default="false",
        validation_alias=AliasChoices("IS_QWEN_LLM", "IS_PHI3_LLM"),
    )

    # OpenAI - from env, then initial_config.txt (OPEN_API_KEY)
    OPENAI_API_KEY: str = ""
    ENABLE_LANGFUSE: bool = False
    LANGFUSE_PUBLIC_KEY: str = ""
    LANGFUSE_SECRET_KEY: str = ""
    LANGFUSE_HOST: str = "https://cloud.langfuse.com"

    # Qwen 3.5 via Ollama (/api/chat); PHI3_* env names kept as legacy aliases
    QWEN_BASE_URL: str = Field(
        default="",
        validation_alias=AliasChoices("QWEN_BASE_URL", "PHI3_BASE_URL"),
    )
    QWEN_MODEL: str = Field(
        default="qwen3.5:4b",
        validation_alias=AliasChoices("QWEN_MODEL", "PHI3_MODEL"),
    )
    # Outbound REST / SAP OData timeout (seconds). Default = 5 minutes.
    # Legacy deployments may store this in milliseconds (e.g. 300000); fill_from_initial_config
    # normalises any value >= 10000 by integer-dividing by 1000 so we always end up in seconds.
    TIMEOUT: int = 300

    # Google Gemini (e.g. @askme data analysis with google_search) — env, then initial_config.txt
    # (gemini_api_key, GEMINI_API_KEY, or GOOGLE_API_KEY)
    GEMINI_API_KEY: str = ""

    # Anthropic Claude (direct API when use_claude=true and oci_call=false)
    CLAUDE_API_KEY: str = Field(
        default="",
        validation_alias=AliasChoices("CLAUDE_API_KEY", "ANTHROPIC_API_KEY"),
    )
    CLAUDE_MODEL: str = "claude-sonnet-4-5"

    # Oracle Integration Cloud REST (Anthropic flow) — HTTP Basic; URL from env or initial_config.txt
    OIC_ANTHROPIC_MESSAGE_URL: str = ""

    OIC_HTTP_USERNAME: str = Field(
        default="",
        validation_alias=AliasChoices("OIC_HTTP_USERNAME", "OIC_NONPRD_ADMINUSER"),
    )
    OIC_HTTP_PASSWORD: str = Field(
        default="",
        validation_alias=AliasChoices("OIC_HTTP_PASSWORD", "OIC_NONPRD_ADMINPASSWORD"),
    )
    TESSERACT_PATH: str = ""
    # Qwen+@askme: Gemini+Google Search using the user question only (no DF rows to Gemini). Set false to disable.
    USE_GEMINI_GOOGLE_SEARCH_WITH_QWEN: bool = True

    # Per file_path folder upload schedule: 30 min cycle = 20 min active batches + 10 min rest.
    FOLDER_SCAN_SCHEDULE_CYCLE_MINUTES: int = 30
    FOLDER_SCAN_SCHEDULE_ACTIVE_MINUTES: int = 20
    FOLDER_SCAN_SCHEDULE_REST_MINUTES: int = 10

    # Path to initial_config.txt (relative to project root or absolute)
    INITIAL_CONFIG_FILE: str = "initial_config.txt"

    # Fernet key for automation connector credentials (generate with Fernet.generate_key())
    AUTOMATION_ENCRYPTION_KEY: str = ""

    @model_validator(mode="after")
    def fill_from_initial_config(self):
        cfg = get_initial_config()
        if not (self.OPENAI_API_KEY or str(self.OPENAI_API_KEY).strip()):
            key = (cfg.get("OPEN_API_KEY") or cfg.get("OPENAI_API_KEY") or "").strip()
            if key:
                object.__setattr__(self, "OPENAI_API_KEY", key)
        if not (self.GEMINI_API_KEY or str(self.GEMINI_API_KEY).strip()):
            gkey = (
                cfg.get("gemini_api_key")
                or cfg.get("GEMINI_API_KEY")
                or cfg.get("GOOGLE_API_KEY")
                or ""
            ).strip()
            if gkey:
                object.__setattr__(self, "GEMINI_API_KEY", gkey)

        if not (self.CLAUDE_API_KEY or str(self.CLAUDE_API_KEY).strip()):
            ckey = (
                cfg.get("claude_api_key")
                or cfg.get("CLAUDE_API_KEY")
                or ""
            ).strip()
            if ckey:
                object.__setattr__(self, "CLAUDE_API_KEY", ckey)
        cm = (cfg.get("CLAUDE_MODEL") or cfg.get("claude_model") or "").strip()
        if cm:
            object.__setattr__(self, "CLAUDE_MODEL", cm)

        # Local Ollama Qwen
        qwen_url = (cfg.get("QWEN_BASE_URL") or cfg.get("PHI3_BASE_URL") or "").strip()
        if qwen_url:
            object.__setattr__(self, "QWEN_BASE_URL", qwen_url)

        qwen_model = (cfg.get("QWEN_MODEL") or cfg.get("PHI3_MODEL") or "").strip()
        if qwen_model:
            object.__setattr__(self, "QWEN_MODEL", qwen_model)

        raw_to = (cfg.get("TIMEOUT") or "").strip()
        if raw_to:
            try:
                parsed_to = int(raw_to)
                # Legacy values were stored in milliseconds (e.g. 300000). Treat anything
                # >= 10_000 as ms and convert to seconds; small values are taken as seconds.
                if parsed_to >= 10000:
                    parsed_to = parsed_to // 1000
                if parsed_to <= 0:
                    parsed_to = 300
                object.__setattr__(self, "TIMEOUT", parsed_to)
            except ValueError:
                pass
        raw_db_to = (cfg.get("TIMEOUT_DB") or "").strip()
        if raw_db_to:
            try:
                parsed_db_to = int(raw_db_to)
                # Legacy values may be stored in milliseconds (e.g. 300000).
                if parsed_db_to >= 10000:
                    parsed_db_to = parsed_db_to // 1000
                if parsed_db_to <= 0:
                    parsed_db_to = 300
                object.__setattr__(self, "TIMEOUT_DB", parsed_db_to)
            except ValueError:
                pass
        raw_llm = (
            cfg.get("IS_QWEN_LLM")
            or cfg.get("IS_PHI3_LLM")
            or cfg.get("is_qwen_llm")
            or cfg.get("is_phi3_llm")
            or ""
        ).strip()
        if raw_llm:
            object.__setattr__(self, "IS_QWEN_LLM", raw_llm)
        if not (self.LANGFUSE_PUBLIC_KEY or str(self.LANGFUSE_PUBLIC_KEY).strip()):
            langfuse_public_key = (cfg.get("LANGFUSE_PUBLIC_KEY") or "").strip()
            if langfuse_public_key:
                object.__setattr__(self, "LANGFUSE_PUBLIC_KEY", langfuse_public_key)
        if not (self.LANGFUSE_SECRET_KEY or str(self.LANGFUSE_SECRET_KEY).strip()):
            langfuse_secret_key = (cfg.get("LANGFUSE_SECRET_KEY") or "").strip()
            if langfuse_secret_key:
                object.__setattr__(self, "LANGFUSE_SECRET_KEY", langfuse_secret_key)
        raw_enable_langfuse = (cfg.get("ENABLE_LANGFUSE") or "").strip().lower()
        if raw_enable_langfuse:
            object.__setattr__(
                self,
                "ENABLE_LANGFUSE",
                raw_enable_langfuse in ("true", "1", "yes", "y", "on"),
            )
        current_host = (self.LANGFUSE_HOST or "").strip()
        langfuse_host = (cfg.get("LANGFUSE_HOST") or "").strip()
        # LANGFUSE_HOST has a non-empty default; allow initial_config override when
        # unset or still on the default cloud host.
        if langfuse_host and (not current_host or current_host == "https://cloud.langfuse.com"):
            object.__setattr__(self, "LANGFUSE_HOST", langfuse_host)
        if not (self.OIC_ANTHROPIC_MESSAGE_URL or "").strip():
            oic_url = (
                (cfg.get("OIC_ANTHROPIC_MESSAGE_URL") or cfg.get("oic_anthropic_message_url") or "")
                .strip()
            )
            if oic_url:
                object.__setattr__(self, "OIC_ANTHROPIC_MESSAGE_URL", oic_url)
        if not (self.OIC_HTTP_USERNAME or "").strip():
            oic_user = (
                cfg.get("OIC_HTTP_USERNAME")
                or cfg.get("oic_http_username")
                or cfg.get("OIC_NONPRD_ADMINUSER")
                or ""
            ).strip()
            if oic_user:
                object.__setattr__(self, "OIC_HTTP_USERNAME", oic_user)
        if not (self.OIC_HTTP_PASSWORD or "").strip():
            oic_pwd = (
                cfg.get("OIC_HTTP_PASSWORD")
                or cfg.get("oic_http_password")
                or cfg.get("OIC_NONPRD_ADMINPASSWORD")
                or ""
            ).strip()
            if oic_pwd:
                object.__setattr__(self, "OIC_HTTP_PASSWORD", oic_pwd)
        raw_gws = (cfg.get("USE_GEMINI_GOOGLE_SEARCH_WITH_QWEN") or "").strip().lower()
        if raw_gws:
            object.__setattr__(
                self,
                "USE_GEMINI_GOOGLE_SEARCH_WITH_QWEN",
                raw_gws in ("true", "1", "yes", "y", "on"),
            )
        raw_sql_sum = (cfg.get("ENABLE_SQL_PLAIN_SUMMARY") or "").strip().lower()
        if raw_sql_sum:
            object.__setattr__(
                self,
                "ENABLE_SQL_PLAIN_SUMMARY",
                raw_sql_sum in ("true", "1", "yes", "y", "on"),
            )
        raw_check_file = (cfg.get("CHECK_FILE") or cfg.get("check_file") or "").strip().lower()
        if raw_check_file:
            object.__setattr__(
                self,
                "CHECK_FILE",
                raw_check_file in ("true", "1", "yes", "y", "on"),
            )
        return self

    def llm_mode(self) -> LlmMode:
        """true → qwen_only; false → openai_only; auto (or legacy hybrid) → auto. Legacy phi3_only maps to qwen_only."""
        v = (self.IS_QWEN_LLM or "").strip().lower()
        if v in ("true", "1", "yes", "phi3_only", "qwen_only"):
            return "qwen_only"
        if v in ("false", "0", "no", "openai_only"):
            return "openai_only"
        return "auto"

    def is_local_qwen_configured(self) -> bool:
        """True when Ollama endpoints are set (QWEN_BASE_URL and QWEN_MODEL)."""
        return bool((self.QWEN_BASE_URL or "").strip() and (self.QWEN_MODEL or "").strip())

    def default_use_qwen_for_summary_and_web(self) -> bool:
        """
        True when summary / @web / @askme may route to local Qwen: not openai_only and
        Ollama (QWEN_BASE_URL + QWEN_MODEL) is configured. Otherwise the app uses the
        same models as before (e.g. OpenAI; @askme analysis may still use Gemini when
        a Gemini key is set and local Qwen is not used).
        """
        if self.llm_mode() == "openai_only":
            return False
        return self.is_local_qwen_configured()

    class Config:
        env_file = ".env"
        extra = "ignore"


settings = Settings()


def api_request_timeout_seconds() -> int:
    """Return the configured outbound API timeout in seconds (>= 1).

    Reads ``settings.TIMEOUT`` lazily so callers always pick up the latest value.
    Used by REST and SAP OData clients in ``app.services.api_call``.
    """
    try:
        value = int(getattr(settings, "TIMEOUT", 300) or 300)
    except (TypeError, ValueError):
        value = 300
    # Cap value to prevent OverflowError on Windows (max safe value is around 2147 seconds)
    return min(2000, max(1, value))


def db_statement_timeout_ms() -> int:
    """Return TIMEOUT_DB in milliseconds for Oracle/MySQL server-side statement limit."""
    try:
        value = int(getattr(settings, "TIMEOUT_DB", 300) or 300)
    except (TypeError, ValueError):
        value = 300
    return max(1, value) * 1000
