from __future__ import annotations

from datetime import datetime, timedelta
import re

import pandas as pd
import streamlit as st
import yfinance as yf


_DAY_PERIOD_PATTERN = re.compile(r"^([1-9]\d*)d$")
_DAY_PERIOD_FETCH_ATTEMPTS = 5


def _utc_now() -> datetime:
    return datetime.utcnow()


def _parse_day_period(period: str) -> int | None:
    match = _DAY_PERIOD_PATTERN.match(str(period).strip().lower())
    if not match:
        return None
    return int(match.group(1))


def _intraday_max_lookback_days(interval: str) -> int | None:
    interval_key = str(interval).strip().lower()
    if interval_key == "1m":
        return 7
    if interval_key in {"2m", "5m", "15m", "30m", "60m", "90m", "1h"}:
        return 60
    return None


def _initial_calendar_lookback_days(trading_days: int) -> int:
    # Expand beyond target trading days to account for weekends and holidays.
    return max(trading_days + 2, int((trading_days * 7) / 5) + 3)


def _trading_day_count(df: pd.DataFrame) -> int:
    if df.empty:
        return 0
    index = pd.DatetimeIndex(df.index)
    return int(index.normalize().nunique())


def _trim_to_recent_trading_days(df: pd.DataFrame, trading_days: int) -> pd.DataFrame:
    if df.empty or trading_days <= 0:
        return df.copy()

    index = pd.DatetimeIndex(df.index)
    session_days = pd.DatetimeIndex(index.normalize().unique()).sort_values()
    if len(session_days) <= trading_days:
        return df.copy()

    keep_days = session_days[-trading_days:]
    mask = index.normalize().isin(keep_days)
    return df.loc[mask].copy()


def _fetch_history_for_period(ticker: yf.Ticker, interval: str, period: str) -> pd.DataFrame:
    history_kwargs = {"interval": interval, "auto_adjust": False, "actions": False}
    day_period = _parse_day_period(period)
    if day_period is None:
        return ticker.history(period=period, **history_kwargs)

    lookback_days = _initial_calendar_lookback_days(day_period)
    max_lookback_days = _intraday_max_lookback_days(interval)
    if max_lookback_days is not None:
        lookback_days = min(lookback_days, max_lookback_days)

    now = _utc_now()
    best_df = pd.DataFrame()
    for _ in range(_DAY_PERIOD_FETCH_ATTEMPTS):
        start = now - timedelta(days=lookback_days)
        df = ticker.history(start=start, end=now, **history_kwargs)
        if len(df) > len(best_df):
            best_df = df

        if _trading_day_count(df) >= day_period:
            return df

        if max_lookback_days is not None and lookback_days >= max_lookback_days:
            break

        next_lookback = int(lookback_days * 1.6) + 1
        if max_lookback_days is not None:
            next_lookback = min(next_lookback, max_lookback_days)
        if next_lookback <= lookback_days:
            break
        lookback_days = next_lookback

    if not best_df.empty:
        return best_df
    return ticker.history(period=period, **history_kwargs)


@st.cache_data(ttl=60, show_spinner=False)
def fetch_ohlc(symbol: str, interval: str, period: str) -> pd.DataFrame:
    ticker = yf.Ticker(symbol)
    df = _fetch_history_for_period(ticker=ticker, interval=interval, period=period)
    if df.empty:
        raise ValueError("No data returned. Check symbol/interval/period compatibility.")

    df = df.rename(columns=str.title)
    required = ["Open", "High", "Low", "Close", "Volume"]
    missing = [c for c in required if c not in df.columns]
    if missing:
        raise ValueError(f"Missing required columns: {missing}")

    out = df[required].dropna().copy()
    day_period = _parse_day_period(period)
    if day_period is not None:
        out = _trim_to_recent_trading_days(out, day_period)

    return out


def maybe_drop_live_bar(df: pd.DataFrame, interval: str, enabled: bool) -> pd.DataFrame:
    if not enabled or len(df) < 2:
        return df

    last_ts = df.index[-1]
    if last_ts.tzinfo is None:
        now = datetime.utcnow()
    else:
        now = datetime.now(tz=last_ts.tzinfo)

    delta = now - last_ts.to_pydatetime()

    intraday_intervals = {
        "1m": 1,
        "2m": 2,
        "5m": 5,
        "15m": 15,
        "30m": 30,
        "60m": 60,
        "90m": 90,
        "1h": 60,
    }
    minutes = intraday_intervals.get(interval)
    if minutes is not None and delta.total_seconds() < minutes * 60:
        return df.iloc[:-1].copy()

    return df
