import os
from typing import Any, Optional

import pandas as pd

from factordb.core.expression import ExpressionCalculator

from .config import FactorAnalysisConfig


class FactorAnalysis:
    """Facade for computing factor expressions with flexible data loading."""

    def __init__(self, config: FactorAnalysisConfig):
        self.config = config
        self.calculator = ExpressionCalculator()

    def _get_clickhouse_connection_kwargs(self) -> dict[str, Any]:
        """Build standalone ClickHouse connection kwargs without FactorDB DB routing."""
        host = self.config.get_clickhouse_option("host") or os.getenv("CLICKHOUSE_HOST") or os.getenv("DB_HOST", "localhost")
        port = int(self.config.get_clickhouse_option("port") or os.getenv("CLICKHOUSE_PORT") or os.getenv("DB_PORT", 9000))
        user = self.config.get_clickhouse_option("user") or os.getenv("CLICKHOUSE_USER") or os.getenv("DB_USER", "default")
        password = self.config.get_clickhouse_option("password")
        if password is None:
            password = os.getenv("CLICKHOUSE_PASSWORD") or os.getenv("DB_PASSWORD", "")

        kwargs: dict[str, Any] = {
            "host": host,
            "port": port,
            "user": user,
            "password": password,
        }

        database = self.config.get_clickhouse_option("database") or os.getenv("CLICKHOUSE_DATABASE")
        if database:
            kwargs["database"] = database

        settings = self.config.get_clickhouse_option("settings")
        if settings:
            kwargs["settings"] = settings

        return kwargs

    def _build_clickhouse_sql(self) -> str:
        """Build a raw SQL query for standalone ClickHouse reads."""
        query = self.config.get_clickhouse_option("query")
        if query:
            return str(query)

        table = self.config.get_clickhouse_option("table")
        if not table:
            raise ValueError("clickhouse.table or clickhouse.query is required for clickhouse data_source")

        database = self.config.get_clickhouse_option("database")
        from_clause = table if "." in str(table) else f"{database}.{table}" if database else str(table)

        columns = self.config.get_clickhouse_option("columns", "*")
        if isinstance(columns, (list, tuple)):
            select_columns = ", ".join(str(col) for col in columns)
        else:
            select_columns = str(columns)

        sql = f"SELECT {select_columns} FROM {from_clause}"
        where = self.config.get_clickhouse_option("where")
        if where:
            sql += f" WHERE {where}"

        order_by = self.config.get_clickhouse_option("order_by")
        if order_by:
            sql += f" ORDER BY {order_by}"

        limit = self.config.get_clickhouse_option("limit")
        if limit:
            sql += f" LIMIT {int(limit)}"

        return sql

    def _load_from_clickhouse(self) -> pd.DataFrame:
        """Load raw source data directly from ClickHouse without FactorDB abstractions."""
        try:
            from clickhouse_driver import Client
        except ImportError as exc:
            raise ImportError(
                "clickhouse-driver is required for factor_analysis ClickHouse mode. "
                "Install it with `pip install clickhouse-driver`."
            ) from exc

        sql = self._build_clickhouse_sql()
        client = Client(**self._get_clickhouse_connection_kwargs())
        try:
            rows, meta = client.execute(sql, with_column_types=True)
            columns = [name for name, _ in meta]
            return pd.DataFrame(rows, columns=columns)
        finally:
            client.disconnect()

    def load_data(self) -> pd.DataFrame:
        """Load market data according to config."""
        source = self.config.data_source.lower()
        if source in {"csv", "parquet"}:
            if not self.config.data_path:
                raise ValueError("data_path is required for csv/parquet data_source")
            path = self.config.data_path
            if path.endswith(".parquet"):
                return pd.read_parquet(path)
            return pd.read_csv(path)

        if source == "clickhouse":
            return self._load_from_clickhouse()

        raise ValueError(f"Unsupported data_source: {self.config.data_source}")

    def calculate(self, expression: str, data: Optional[pd.DataFrame] = None, source_framework: str = "auto"):
        """Calculate factor values; returns Series with MultiIndex (code, date)."""
        df = data if data is not None else self.load_data()
        return self.calculator.calculate(
            expression=expression,
            data=df,
            code_column=self.config.code_column,
            date_column=self.config.date_column,
            source_framework=source_framework,
        )

    def calculate_cross_section(self, expression: str, data: Optional[pd.DataFrame] = None, source_framework: str = "auto"):
        """Calculate cross-sectional values for a given date-indexed matrix."""
        df = data if data is not None else self.load_data()
        return self.calculator.calculate_cross_section(
            expression=expression,
            data=df,
            source_framework=source_framework,
        )

    def calculate_batch(
        self,
        expressions: dict[str, str],
        data: Optional[pd.DataFrame] = None,
        source_framework: str = "auto",
    ) -> pd.DataFrame:
        """Calculate multiple expressions against the same loaded dataset."""
        df = data if data is not None else self.load_data()
        series_map: dict[str, pd.Series] = {}
        for name, expression in expressions.items():
            series_map[name] = self.calculate(
                expression=expression,
                data=df,
                source_framework=source_framework,
            )
        return pd.concat(series_map, axis=1)
