"""Execution algorithms: TWAP, VWAP, Iceberg."""

from __future__ import annotations

import logging
import time
from abc import ABC, abstractmethod

from horizon._horizon import Engine, OrderRequest


class ExecAlgo(ABC):
    """Base class for execution algorithms."""

    def __init__(self, engine: Engine):
        self.engine = engine
        self._parent: OrderRequest | None = None
        self._child_ids: list[str] = []
        self._total_filled: float = 0.0
        self._target_size: float = 0.0
        self._complete = False

    def start(self, request: OrderRequest) -> None:
        """Begin execution of a parent order."""
        self._parent = request
        self._target_size = request.size
        self._total_filled = 0.0
        self._child_ids = []
        self._complete = False
        self._on_start()

    @abstractmethod
    def _on_start(self) -> None:
        """Hook called when execution starts."""

    @abstractmethod
    def on_tick(self, current_price: float, timestamp: float) -> None:
        """Called each cycle to manage child orders."""

    @property
    def is_complete(self) -> bool:
        return self._complete

    @property
    def child_order_ids(self) -> list[str]:
        return list(self._child_ids)

    @property
    def total_filled(self) -> float:
        return self._total_filled

    def _submit_child(self, size: float, price: float) -> str | None:
        """Submit a child order slice."""
        if self._parent is None or self._complete:
            return None
        remaining = self._target_size - self._total_filled
        if remaining <= 0:
            self._complete = True
            return None
        actual_size = min(size, remaining)
        if actual_size <= 0:
            return None
        req = OrderRequest(
            market_id=self._parent.market_id,
            side=self._parent.side,
            order_side=self._parent.order_side,
            size=actual_size,
            price=price,
            order_type=self._parent.order_type,
            time_in_force=self._parent.time_in_force,
            post_only=self._parent.post_only,
            token_id=self._parent.token_id,
            neg_risk=self._parent.neg_risk,
        )
        try:
            order_id = self.engine.submit_order(req)
            self._child_ids.append(order_id)
            return order_id
        except Exception as e:
            logging.getLogger("horizon.algos").warning(
                "Child order submission failed: %s", e
            )
            return None

    def _update_filled(self) -> None:
        """Check engine fills for child orders and update total_filled."""
        fills = self.engine.recent_fills()
        child_set = set(self._child_ids)
        filled = 0.0
        for fill in fills:
            if fill.order_id in child_set:
                filled += fill.size
        self._total_filled = filled
        if self._total_filled >= self._target_size - 1e-10:
            self._complete = True


class TWAP(ExecAlgo):
    """Time-Weighted Average Price: splits into equal slices over a duration."""

    def __init__(self, engine: Engine, duration_secs: float, num_slices: int):
        super().__init__(engine)
        self.duration_secs = duration_secs
        self.num_slices = max(num_slices, 1)
        self._slice_interval: float = 0.0
        self._next_slice_time: float = 0.0
        self._slices_sent: int = 0
        self._slice_size: float = 0.0

    def _on_start(self) -> None:
        self._slice_interval = self.duration_secs / self.num_slices
        self._next_slice_time = time.time()
        self._slices_sent = 0
        self._slice_size = self._target_size / self.num_slices

    def on_tick(self, current_price: float, timestamp: float) -> None:
        if self._complete or self._parent is None:
            return
        self._update_filled()
        if self._complete:
            return

        now = time.time()
        while (
            self._slices_sent < self.num_slices
            and now >= self._next_slice_time
            and not self._complete
        ):
            self._submit_child(self._slice_size, current_price)
            self._slices_sent += 1
            self._next_slice_time += self._slice_interval

        if self._slices_sent >= self.num_slices:
            self._update_filled()


class Iceberg(ExecAlgo):
    """Iceberg: shows only a visible portion at a time."""

    def __init__(self, engine: Engine, show_size: float):
        super().__init__(engine)
        self.show_size = show_size
        self._visible_order_id: str | None = None

    def _on_start(self) -> None:
        self._visible_order_id = None

    def on_tick(self, current_price: float, timestamp: float) -> None:
        if self._complete or self._parent is None:
            return
        self._update_filled()
        if self._complete:
            return

        # Check if visible order is still open
        need_new = False
        if self._visible_order_id is None:
            need_new = True
        else:
            open_orders = self.engine.open_orders()
            still_open = any(o.id == self._visible_order_id for o in open_orders)
            if not still_open:
                need_new = True

        if need_new:
            remaining = self._target_size - self._total_filled
            if remaining <= 0:
                self._complete = True
                return
            visible = min(self.show_size, remaining)
            order_id = self._submit_child(visible, current_price)
            self._visible_order_id = order_id


class VWAP(ExecAlgo):
    """Volume-Weighted Average Price: slices proportional to volume profile."""

    def __init__(
        self, engine: Engine, duration_secs: float, volume_profile: list[float]
    ):
        super().__init__(engine)
        self.duration_secs = duration_secs
        self.volume_profile = volume_profile if volume_profile else [1.0]
        self._slice_interval: float = 0.0
        self._next_slice_time: float = 0.0
        self._slices_sent: int = 0
        self._slice_sizes: list[float] = []

    def _on_start(self) -> None:
        total_weight = sum(self.volume_profile)
        if total_weight <= 0:
            total_weight = 1.0
        self._slice_sizes = [
            (w / total_weight) * self._target_size for w in self.volume_profile
        ]
        num = len(self._slice_sizes)
        self._slice_interval = self.duration_secs / num
        self._next_slice_time = time.time()
        self._slices_sent = 0

    def on_tick(self, current_price: float, timestamp: float) -> None:
        if self._complete or self._parent is None:
            return
        self._update_filled()
        if self._complete:
            return

        now = time.time()
        while (
            self._slices_sent < len(self._slice_sizes)
            and now >= self._next_slice_time
            and not self._complete
        ):
            self._submit_child(
                self._slice_sizes[self._slices_sent], current_price
            )
            self._slices_sent += 1
            self._next_slice_time += self._slice_interval

        if self._slices_sent >= len(self._slice_sizes):
            self._update_filled()
