import logging
import re
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from .enums import AssetType
from .storage import list_parquet_files, load_parquet_file

# Set up logger
logger = logging.getLogger(__name__)

if not logger.handlers:
    handler = logging.StreamHandler()
    formatter = logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)
logger.setLevel(logging.ERROR)  # Default log level is ERROR


def _parse_month(month_str: str) -> datetime:
    """Parse a month string formatted as 'YYYY.MM' into a datetime object."""
    return datetime.strptime(month_str, "%Y.%m")


def _validate_contiguity(months: list[str]):
    """
    Check if the list of month strings is contiguous.
    The list must contain consecutive months without any missing month.
    Raises ValueError if there is a gap.
    """
    if not months:
        raise ValueError("No month files found.")
    parsed = sorted([_parse_month(m) for m in months])
    current = parsed[0]
    for dt in parsed[1:]:
        # Compute the expected next month
        year = current.year + (current.month // 12)
        month = current.month % 12 + 1
        expected = current.replace(year=year, month=month)
        if dt.year != expected.year or dt.month != expected.month:
            raise ValueError(
                f"Missing month: expected {expected.strftime('%Y.%m')} after {current.strftime('%Y.%m')}, "
                f"but got {dt.strftime('%Y.%m')}"
            )
        current = dt


def load_daily(
    asset_type: AssetType,
    month_range: tuple[str, str] | None = None,
    threads: int = 4,
    adjust: bool = True,
    to_usd: bool = True,
    rate_to_price: bool = True,
    token: dict | None = None,
    cache: bool = False,
) -> pd.DataFrame:
    """
    Load the daily data for the specified asset type and merge into one DataFrame.

    Parameters:
        asset_type: An AssetType enum value (Stocks, ETFs, Indices, or Cryptocurrencies).
        bucket_name: The R2 bucket name (default is "alpha").
        month_range: Optional tuple (start, end) with month strings in 'YYYY.MM' format.
                     If None, load all available months and validate contiguity.
        threads: Number of threads to use for concurrent loading (default is 4).
        token: A dictionary containing R2 credentials with keys: R2_ENDPOINT_URL, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY.
               If None, environment variables are used.
        cache: Whether to use caching for loading parquet files (default is False).

    Returns:
        A merged pandas DataFrame containing the data from all loaded parquet files.
    """
    # Get repo_id from asset_type and compute repo_name (last part of the repo_id in lowercase)
    repo_id = asset_type.value
    repo_name = repo_id.split("/")[-1].lower()
    logger.debug(f"Loading data: repo_id={repo_id}, repo_name={repo_name}")

    # List all parquet files under ds/<repo_name>/ on R2
    file_dict = list_parquet_files(repo_name=repo_name, token=token)
    if not file_dict:
        raise ValueError(f"No parquet files found for repo {repo_name}.")
    available_months = sorted(file_dict.keys())
    logger.debug(f"Available months: {available_months}")

    # Filter by month_range if provided; otherwise, validate the entire range for contiguity
    if month_range:
        start, end = month_range
        filtered_months = [m for m in available_months if start <= m <= end]
        if not filtered_months:
            raise ValueError("No parquet files found within the specified month range.")
        _validate_contiguity(filtered_months)
        selected_months = filtered_months
    else:
        _validate_contiguity(available_months)
        selected_months = available_months

    logger.info(f"Selected months: {selected_months}")

    # Concurrently load each month's parquet file using a thread pool
    dfs = []
    with ThreadPoolExecutor(max_workers=threads) as executor:
        future_to_month = {
            executor.submit(
                load_parquet_file,
                repo_name=repo_name,
                month=month,
                token=token,
                cache=cache,
            ): month
            for month in selected_months
        }
        for future in as_completed(future_to_month):
            month = future_to_month[future]
            try:
                df = future.result()
                logger.debug(
                    f"Successfully loaded month {month} with {len(df)} records"
                )
                dfs.append(df)
            except Exception as exc:
                logger.error(f"Error loading month {month}: {exc}")
                raise exc

    if not dfs:
        raise ValueError("No data loaded.")
    combined_df = (
        pd.concat(dfs, ignore_index=True)
        .sort_values(["date", "symbol"])
        .reset_index(drop=True)
    )

    combined_df["date"] = pd.to_datetime(combined_df["date"])

    if adjust and "adj_close" in combined_df.columns:
        adj_factor = combined_df["adj_close"] / combined_df["close"]
        combined_df["adj_open"] = combined_df["open"] * adj_factor
        combined_df["adj_high"] = combined_df["high"] * adj_factor
        combined_df["adj_low"] = combined_df["low"] * adj_factor
        combined_df.drop(columns=["open", "high", "low", "close"], inplace=True)
        combined_df.rename(
            columns={
                "adj_open": "open",
                "adj_high": "high",
                "adj_low": "low",
                "adj_close": "close",
            },
            inplace=True,
        )
    else:
        if "adj_close" in combined_df.columns:
            combined_df.drop(columns=["adj_close"])

    if to_usd:
        if asset_type == AssetType.Forex:
            for index, row in combined_df.iterrows():
                if row["symbol"].endswith("USD"):
                    continue
                combined_df.at[index, "open"] = 1 / row["open"]
                combined_df.at[index, "high"] = 1 / row["high"]
                combined_df.at[index, "low"] = 1 / row["low"]
                combined_df.at[index, "close"] = 1 / row["close"]
                combined_df.at[index, "symbol"] = row["symbol"][3:] + "USD"
        elif asset_type == AssetType.Indices:
            df_forex = load_daily(
                AssetType.Forex, token=token, cache=cache, to_usd=True
            )
            combined_df = __convert_indices_to_usd(combined_df, df_forex)

    if rate_to_price and asset_type == AssetType.Bonds:
        for index, row in combined_df.iterrows():
            years_to_maturity = __extract_years_to_maturity(row["symbol"])
            if not years_to_maturity:
                continue
            face_value = 100
            for col in ["open", "high", "low", "close"]:
                rate = row[col]
                combined_df.loc[index, col] = (
                    face_value / (1 + rate) ** years_to_maturity
                )

    combined_df = combined_df.sort_values(["date", "symbol"]).reset_index(drop=True)

    logger.info(f"Merged DataFrame record count: {len(combined_df)}")
    return combined_df


def __extract_years_to_maturity(bond_symbol):
    match = re.search(r"(\d+)([YM])$", bond_symbol)
    if match:
        time_value = int(match.group(1))  # Extract the numeric value
        time_unit = match.group(2)  # Extract the time unit (Y or M)
        if time_unit == "Y":
            return time_value  # It's already in years
        elif time_unit == "M":
            return time_value / 12  # Convert months to years


def __convert_indices_to_usd(df_indices, df_forex):
    mapping = {
        "ADSMI": "AED",  # United Arab Emirates
        "AEX": "EUR",  # Netherlands
        "AS30": "AUD",  # Australia
        "AS51": "AUD",  # Australia
        "AS52": "AUD",  # Australia
        "ASE": "EUR",  # Greece
        "ATX": "EUR",  # Austria
        "BEL20": "EUR",  # Belgium
        "BELEX15": "RSD",  # Serbia
        "BGSMDC": "BWP",  # Botswana
        "BHSEEI": "BHD",  # Bahrain
        "BKA": "BAM",  # Bosnia and Herzegovina
        "BLOM": "LBP",  # Lebanon
        "BSX": "BMD",  # Bermuda
        "BUX": "HUF",  # Hungary
        "BVLX": "BOB",  # Bolivia
        "BVPSBVPS": "PAB",  # Panama
        "BVQA": "USD",  # Ecuador
        "CAC": "EUR",  # France
        "CASE": "EGP",  # Egypt
        "CCMP": "USD",  # United States
        "COLCAP": "COP",  # Colombia
        "CRSMBCT": "CRC",  # Costa Rica
        "CSEALL": "LKR",  # Sri Lanka
        "CYSMMAPA": "EUR",  # Cyprus
        "DARSDSEI": "TZS",  # Tanzania
        "DAX": "EUR",  # Germany
        "DFMGI": "AED",  # United Arab Emirates
        "DSEX": "BDT",  # Bangladesh
        "DSM": "QAR",  # Qatar
        "ECU": "USD",  # Ecuador
        "FBMKLCI": "MYR",  # Malaysia
        "FSSTI": "SGD",  # Singapore
        "FTN098": "NAD",  # Namibia
        "FTSEMIB": "EUR",  # Italy
        "GGSECI": "GHS",  # Ghana
        "HEX": "EUR",  # Finland
        "HEX25": "EUR",  # Finland
        "HSI": "HKD",  # Hong Kong
        "IBEX": "EUR",  # Spain
        "IBOV": "BRL",  # Brazil
        "IBVC": "VES",  # Venezuela
        "ICEXI": "ISK",  # Iceland
        "IGPA": "CLP",  # Chile
        "INDEXCF": "RUB",  # Russia
        "INDU": "USD",  # United States
        "INDZI": "IDR",  # Indonesia
        "ISEQ": "EUR",  # Ireland
        "JALSH": "ZAR",  # South Africa
        "JCI": "IDR",  # Indonesia
        "JMSMX": "JMD",  # Jamaica
        "JOSMGNFF": "JOD",  # Jordan
        "KFX": "DKK",  # Denmark
        "KNSMIDX": "KES",  # Kenya
        "KSE100": "PKR",  # Pakistan
        "KZKAK": "KZT",  # Kazakhstan
        "LSXC": "LAK",  # Laos
        "LUXXX": "EUR",  # Luxembourg
        "MALTEX": "EUR",  # Malta
        "MBI": "MKD",  # North Macedonia
        "MERVAL": "ARS",  # Argentina
        "MEXBOL": "MXN",  # Mexico
        "MONEX": "EUR",  # Montenegro
        "MOSENEW": "MAD",  # Morocco
        "MSETOP": "MKD",  # North Macedonia
        "MSM30": "OMR",  # Oman
        "NDX": "USD",  # United States
        "NGSEINDX": "NGN",  # Nigeria
        "NIFTY": "INR",  # India
        "NKY": "JPY",  # Japan
        "NSEASI": "KES",  # Kenya
        "NZSE50FG": "NZD",  # New Zealand
        "OMX": "SEK",  # Sweden
        "OSEAX": "NOK",  # Norway
        "PCOMP": "PHP",  # Philippines
        "PFTS": "UAH",  # Ukraine
        "PSI20": "EUR",  # Portugal
        "PX": "CZK",  # Czech Republic
        "RIGSE": "EUR",  # Latvia
        "RTY": "USD",  # United States
        "SASEIDX": "SAR",  # Saudi Arabia
        "SASX10": "BAM",  # Bosnia and Herzegovina
        "SBITOP": "EUR",  # Slovenia
        "SEMDEX": "MUR",  # Mauritius
        "SENSEX": "INR",  # India
        "SET50": "THB",  # Thailand
        "SHCOMP": "CNY",  # China
        "SHSZ300": "CNY",  # China
        "SKSM": "EUR",  # Slovakia
        "SMI": "CHF",  # Switzerland
        "SOFIX": "BGN",  # Bulgaria
        "SPBLPGPT": "PEN",  # Peru
        "SPTSX": "CAD",  # Canada
        "SPX": "USD",  # United States
        "SSE50": "CNY",  # China
        "SX5E": "EUR",  # Europe
        "TA125": "ILS",  # Israel
    }
    symbols = df_indices.symbol.unique()
    mapping = {k: v for k, v in mapping.items() if k in symbols}
    frames = []
    for symbol, currency in mapping.items():
        df_index = df_indices[df_indices["symbol"] == symbol].copy()
        if currency == "USD":
            frames.append(df_index)
            continue
        df_forex_currency = df_forex[df_forex["symbol"] == currency + "USD"].copy()
        if df_index.empty or df_forex_currency.empty:
            continue
        # Merge dataframes on the date column
        merged_df = pd.merge(
            df_index, df_forex_currency, on="date", suffixes=("", "_forex")
        )

        # Multiply the index prices by the corresponding forex rates
        merged_df["open"] = merged_df["open"] * merged_df["open_forex"]
        merged_df["high"] = merged_df["high"] * merged_df["high_forex"]
        merged_df["low"] = merged_df["low"] * merged_df["low_forex"]
        merged_df["close"] = merged_df["close"] * merged_df["close_forex"]

        frames.append(merged_df[["symbol", "date", "open", "high", "low", "close"]])

    df = pd.concat(frames, ignore_index=True)
    return df


def list_available_months(
    asset_type: AssetType,
    token: dict | None = None,
) -> list[str]:
    """
    List all available month strings (in 'YYYY.MM' format) for the specified asset type.

    Parameters:
        asset_type: An AssetType enum value (Stocks, ETFs, Indices, or Cryptocurrencies).
        bucket_name: The R2 bucket name (default is "alpha").
        token: A dictionary containing R2 credentials with keys: R2_ENDPOINT_URL, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY.
               If None, environment variables are used.

    Returns:
        A sorted list of month strings (e.g., ['2023.01', '2023.02']).
    """
    repo_id = asset_type.value
    repo_name = repo_id.split("/")[-1].lower()
    file_dict = list_parquet_files(repo_name=repo_name, token=token)
    available_months = sorted(file_dict.keys())
    return available_months
