from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo

from config import settings
from services.paper_executor import paper_executor
from services.runtime_rules import runtime_rules
from services.stop_loss_registry import list_positions
from services.trade_zones import price_in_trade_zones
from services.trade_ledger import RESERVED_OPEN_TTL, history as trade_history, prune_expired_reserved


def _active():
    return runtime_rules.state


def _parse_hhmm(value: str) -> time:
    hour, minute = value.strip().split(":")
    return time(int(hour), int(minute))


def _now_local() -> datetime:
    return datetime.now(ZoneInfo(_active().session_timezone))


def _session_bounds(day: date) -> tuple[datetime, datetime]:
    rules = _active()
    tz = ZoneInfo(rules.session_timezone)
    start = datetime.combine(day, _parse_hhmm(rules.session_start), tzinfo=tz)
    end = datetime.combine(day, _parse_hhmm(rules.session_end), tzinfo=tz)
    return start, end


def is_within_session(now: datetime | None = None) -> bool:
    rules = _active()
    if not rules.session_enabled:
        return True

    return is_within_scheduled_session_hours(now)


def is_within_scheduled_session_hours(now: datetime | None = None) -> bool:
    """Horaire session (debut-fin) sans tenir compte de session_enabled."""
    rules = _active()
    local = now or _now_local()
    if local.tzinfo is None:
        local = local.replace(tzinfo=ZoneInfo(rules.session_timezone))
    else:
        local = local.astimezone(ZoneInfo(rules.session_timezone))

    start, end = _session_bounds(local.date())
    return start <= local <= end


def is_within_usage_hours(now: datetime | None = None) -> bool:
    """Plage horaire d'utilisation Paychek (ex. 9:00 – 22:00). Fin exclusive."""
    rules = _active()
    local = now or _now_local()
    if local.tzinfo is None:
        local = local.replace(tzinfo=ZoneInfo(rules.session_timezone))
    else:
        local = local.astimezone(ZoneInfo(rules.session_timezone))

    start_min = _parse_hhmm(rules.usage_start).hour * 60 + _parse_hhmm(rules.usage_start).minute
    end_min = _parse_hhmm(rules.usage_end).hour * 60 + _parse_hhmm(rules.usage_end).minute
    current_min = local.hour * 60 + local.minute
    if start_min == end_min:
        return False
    if start_min < end_min:
        return start_min <= current_min < end_min
    return current_min >= start_min or current_min < end_min


def discipline_should_be_active(now: datetime | None = None) -> bool:
    rules = _active()
    if not rules.discipline_engaged:
        return False
    local = now or _now_local()
    if local.tzinfo is None:
        local = local.replace(tzinfo=ZoneInfo(rules.session_timezone))
    else:
        local = local.astimezone(ZoneInfo(rules.session_timezone))
    engaged_at = rules.discipline_engaged_at
    if engaged_at is None:
        return is_within_usage_hours(local)
    engaged_local = engaged_at.astimezone(ZoneInfo(rules.session_timezone))
    if engaged_local.date() != local.date():
        return False
    return is_within_usage_hours(local)


def auto_release_discipline_if_needed(now: datetime | None = None) -> bool:
    """Desactive la discipline hors horaires ou apres minuit (nouveau jour)."""
    if discipline_should_be_active(now):
        return False
    rules = _active()
    if not rules.discipline_engaged:
        return False
    from models.companion_rules import CompanionRulesUpdate
    from services.user_settings import apply_companion_rules

    apply_companion_rules(CompanionRulesUpdate(discipline_engaged=False))
    return True


def _hhmm_to_minutes(value: str) -> int:
    hour, minute = value.strip().split(":")
    return int(hour) * 60 + int(minute)


def compute_zones_edit_lock_state(now: datetime | None = None) -> dict:
    """Zones non modifiables tant que la discipline est engagee."""
    rules = _active()
    lock_min = rules.zones_edit_lock_minutes
    if not rules.discipline_engaged:
        return {
            "locked": False,
            "active": False,
            "lock_minutes": lock_min,
            "deadline_label": None,
            "session_start_label": rules.session_start,
            "in_session": False,
        }

    local = now or _now_local()
    if local.tzinfo is None:
        local = local.replace(tzinfo=ZoneInfo(rules.session_timezone))
    else:
        local = local.astimezone(ZoneInfo(rules.session_timezone))
    in_session = is_within_scheduled_session_hours(local)

    return {
        "locked": True,
        "active": True,
        "lock_minutes": lock_min,
        "deadline_label": None,
        "session_start_label": rules.session_start,
        "in_session": in_session,
    }


def _count_window(day: date) -> tuple[datetime, datetime]:
    rules = _active()
    tz = ZoneInfo(rules.session_timezone)
    if rules.session_enabled:
        return _session_bounds(day)
    start = datetime.combine(day, time(0, 0), tzinfo=tz)
    end = datetime.combine(day, time(23, 59, 59), tzinfo=tz)
    return start, end


def _discipline_engaged_at() -> datetime | None:
    rules = _active()
    if not rules.discipline_engaged:
        return None
    return rules.discipline_engaged_at


def _row_at_local(row: dict, tz: ZoneInfo) -> datetime:
    at = datetime.fromisoformat(row["at"])
    if at.tzinfo is None:
        at = at.replace(tzinfo=ZoneInfo("UTC"))
    return at.astimezone(tz)


def _row_counts_for_discipline(row: dict, engaged_at: datetime | None) -> bool:
    """Filtre cooldown / intervalle : ignore les events avant l'activation courante."""
    if engaged_at is None:
        return False
    at = datetime.fromisoformat(row["at"])
    if at.tzinfo is None:
        at = at.replace(tzinfo=ZoneInfo("UTC"))
    return at >= engaged_at


def _reserved_opens_in_session(
    start: datetime,
    end: datetime,
    now_utc: datetime,
) -> int:
    """Reservations encore valides dans la fenetre du jour (compteur journalier)."""
    rules = _active()
    tz = ZoneInfo(rules.session_timezone)
    count = 0
    for row in trade_history():
        if row.get("event") != "open_reserved":
            continue
        at = datetime.fromisoformat(row["at"])
        if at.tzinfo is None:
            at = at.replace(tzinfo=ZoneInfo("UTC"))
        if now_utc - at > RESERVED_OPEN_TTL:
            continue
        at_local = at.astimezone(tz)
        if start <= at_local <= end:
            count += 1
    return count


def trades_opened_in_session(now: datetime | None = None) -> int:
    """Compteur journalier : toutes les ouvertures du jour (session),
    meme apres desactivation / reactivation de la discipline.
    """
    rules = _active()
    local = now or _now_local()
    start, end = _count_window(local.date())
    now_utc = datetime.now(ZoneInfo("UTC"))
    prune_expired_reserved()
    tz = ZoneInfo(rules.session_timezone)

    count = 0
    for row in trade_history():
        if row.get("event") != "open":
            continue
        at_local = _row_at_local(row, tz)
        if start <= at_local <= end:
            count += 1
    return count + _reserved_opens_in_session(start, end, now_utc)


def daily_realized_pnl(now: datetime | None = None) -> float:
    """PnL realise du jour calendaire (persiste apres reactivation)."""
    rules = _active()
    local = now or _now_local()
    day = local.date()
    tz = ZoneInfo(rules.session_timezone)

    total = 0.0
    for row in trade_history():
        if row.get("event") != "close":
            continue
        pnl = row.get("pnl")
        if pnl is None:
            continue
        if _row_at_local(row, tz).date() == day:
            total += float(pnl)
    return total


def daily_loss_pct(now: datetime | None = None) -> float:
    rules = _active()
    if rules.paper_balance <= 0:
        return 0.0
    pnl = daily_realized_pnl(now)
    if pnl >= 0:
        return 0.0
    return abs(pnl) / rules.paper_balance * 100.0


def last_close_event(now: datetime | None = None) -> dict | None:
    rules = _active()
    local = now or _now_local()
    start, end = _count_window(local.date())
    tz = ZoneInfo(rules.session_timezone)
    engaged_at = _discipline_engaged_at()

    latest: dict | None = None
    latest_at: datetime | None = None
    for row in trade_history():
        if row.get("event") != "close":
            continue
        if not _row_counts_for_discipline(row, engaged_at):
            continue
        at = datetime.fromisoformat(row["at"])
        if at.tzinfo is None:
            at = at.replace(tzinfo=ZoneInfo("UTC"))
        at_local = at.astimezone(tz)
        if not (start <= at_local <= end):
            continue
        if latest_at is None or at_local > latest_at:
            latest_at = at_local
            latest = row
    return latest


def trade_interval_remaining_seconds(now: datetime | None = None) -> int:
    rules = _active()
    if not rules.discipline_engaged or not rules.trade_interval_enabled:
        return 0

    last = last_close_event(now)
    if last is None:
        return 0

    pnl = last.get("pnl")
    was_loss = pnl is not None and float(pnl) < 0
    if rules.trade_interval_mode == "after_loss" and not was_loss:
        return 0

    local = now or _now_local()
    at = datetime.fromisoformat(last["at"])
    if at.tzinfo is None:
        at = at.replace(tzinfo=ZoneInfo("UTC"))
    at_local = at.astimezone(local.tzinfo or ZoneInfo(rules.session_timezone))
    elapsed = local - at_local
    wait = timedelta(minutes=rules.trade_interval_minutes)
    remaining = wait - elapsed
    if remaining.total_seconds() < 1:
        return 0
    return int(remaining.total_seconds())


def trade_interval_blocked(now: datetime | None = None) -> tuple[bool, str | None]:
    rules = _active()
    if not rules.discipline_engaged or not rules.trade_interval_enabled:
        return False, None

    remaining = trade_interval_remaining_seconds(now)
    if remaining <= 0:
        return False, None

    mode_label = (
        "after loss"
        if rules.trade_interval_mode == "after_loss"
        else "between each trade"
    )
    minutes_left = max(1, (remaining + 59) // 60)
    return (
        True,
        f"Cooldown {mode_label} — {minutes_left} min left",
    )


def rules_snapshot(now: datetime | None = None) -> dict:
    auto_release_discipline_if_needed(now)
    from services.license_service import license_status

    rules = _active()
    local = now or _now_local()
    lic = license_status()
    pnl = daily_realized_pnl(local)
    trades_left = max(0, rules.max_trades_per_session - trades_opened_in_session(local))
    interval_remaining = trade_interval_remaining_seconds(local)
    last_close = last_close_event(local)
    live_price = rules.live_price
    in_zone = (
        price_in_trade_zones(live_price, rules.trade_zones, ticker=rules.live_price_instrument)
        if live_price is not None and rules.trade_zones_enabled and rules.trade_zones
        else None
    )
    zones_lock = compute_zones_edit_lock_state(local)
    primary_zone = rules.trade_zones[0] if rules.trade_zones else None
    trade_zones_payload = [
        {
            "id": z.id,
            "low": z.low,
            "high": z.high,
            "instrument": z.instrument,
            "label": z.label,
            "side": z.side,
            "direction": z.direction,
            "source": z.source or "addon",
        }
        for z in rules.trade_zones
    ]
    sl_positions = []
    for row in list_positions():
        item = dict(row)
        if rules.sl_modify_limit_enabled:
            item["sl_modify_max"] = rules.sl_modify_max_count
        sl_positions.append(item)
    return {
        "discipline_engaged": rules.discipline_engaged,
        "discipline_engaged_at": (
            rules.discipline_engaged_at.isoformat()
            if rules.discipline_engaged_at
            else None
        ),
        "usage_start": rules.usage_start,
        "usage_end": rules.usage_end,
        "within_usage_hours": is_within_usage_hours(local),
        "trading_enabled": settings.trading_enabled,
        "session_enabled": rules.session_enabled,
        "within_session": is_within_session(local),
        "session_timezone": rules.session_timezone,
        "session_start": rules.session_start,
        "session_end": rules.session_end,
        "max_trades_enabled": rules.max_trades_enabled,
        "daily_loss_limit_enabled": rules.daily_loss_limit_enabled,
        "max_risk_per_trade_enabled": rules.max_risk_per_trade_enabled,
        "max_risk_usd": rules.max_risk_usd,
        "max_risk_per_trade_pct": (
            round(rules.max_risk_usd / rules.paper_balance * 100.0, 2)
            if rules.paper_balance > 0
            else 0.0
        ),
        "paper_balance": rules.paper_balance,
        "rules_source": rules.updated_from,
        "rules_updated_at": rules.updated_at.isoformat() if rules.updated_at else None,
        "pine_strategy": rules.pine_strategy,
        "trades_opened_in_session": trades_opened_in_session(local),
        "trades_remaining": trades_left if rules.max_trades_enabled else None,
        "max_trades_per_session": rules.max_trades_per_session,
        "open_positions": paper_executor.open_positions_count(),
        "max_open_positions": rules.max_open_positions,
        "default_quantity": settings.default_quantity,
        "max_quantity": rules.max_quantity,
        "max_lot_enabled": rules.max_lot_enabled,
        "trade_interval_enabled": rules.trade_interval_enabled,
        "trade_interval_mode": rules.trade_interval_mode,
        "trade_interval_minutes": rules.trade_interval_minutes,
        "trade_interval_remaining_seconds": interval_remaining,
        "last_close_was_loss": (
            float(last_close["pnl"]) < 0
            if last_close is not None and last_close.get("pnl") is not None
            else None
        ),
        "daily_realized_pnl": round(pnl, 2),
        "daily_loss_pct": round(daily_loss_pct(local), 2),
        "max_daily_loss_pct": rules.max_daily_loss_pct,
        "trade_zones_enabled": rules.trade_zones_enabled,
        "trade_zones_count": len(rules.trade_zones),
        "trade_zones": trade_zones_payload,
        "zones_edit_lock_minutes": rules.zones_edit_lock_minutes,
        "zones_edit_locked": zones_lock["locked"],
        "zones_edit_deadline": zones_lock["deadline_label"],
        "zones_edit_in_session": zones_lock["in_session"],
        "trade_zone_low": primary_zone.low if primary_zone else None,
        "trade_zone_high": primary_zone.high if primary_zone else None,
        "trade_zone_instrument": primary_zone.instrument if primary_zone else None,
        "live_price": live_price,
        "live_price_instrument": rules.live_price_instrument,
        "price_in_trade_zone": in_zone,
        "exit_discipline_enabled": rules.exit_discipline_enabled,
        "exit_mode": rules.exit_mode,
        "sl_modify_limit_enabled": rules.sl_modify_limit_enabled,
        "sl_modify_max_count": rules.sl_modify_max_count,
        "sl_breakeven_only_enabled": rules.sl_breakeven_only_enabled,
        "stop_loss_positions": sl_positions,
        "stop_loss_positions_count": len(sl_positions),
        "license_activated": lic.get("activated"),
        "license_plan": lic.get("plan"),
        "license_can_use": lic.get("can_use"),
        "license_reason": lic.get("reason"),
        "license_key_masked": lic.get("license_key_masked"),
        "license_grace_until": lic.get("grace_until"),
        "license_trial_days_left": lic.get("trial_days_left"),
    }
