"""Repair LLM stop/target levels using computed market structure.

The LLM chooses the trade idea. This module only repairs mechanically weak
levels when the already-provided structure supports a better plan. For explicit
reaction-mode trades it may use a measured 1.5R projection from the noise-valid
stop, because that is an executable TP model rather than an invented runner.
"""
from __future__ import annotations

from dataclasses import dataclass, replace
from typing import Any

from .brain import LLMTradeDecision


@dataclass(frozen=True)
class LevelRepairResult:
    allowed: bool
    decision: LLMTradeDecision
    changed: bool
    reasons: list[str]
    metrics: dict[str, Any]

    @property
    def reason(self) -> str:
        return "; ".join(self.reasons)


_TARGET_WARNING_PHRASES = (
    "r:r insufficient",
    "rr insufficient",
    "risk/reward insufficient",
    "below 1.5",
    "below 1.50",
    "under 1.5",
    "target too small",
    "target cannot reach",
    "too small after noise",
    "need farther target",
    "no farther target",
    "farther target not provided",
    "borderline",
)


def repair_trade_levels(
    decision: LLMTradeDecision,
    coin_data: dict[str, Any],
    *,
    min_rr: float,
    min_stop_noise_multiple: float,
    fee_bps: float,
    min_target_pct: float = 0.0,
    min_target_atr_multiple: float = 0.0,
    prefer_htf_targets: bool = True,
    runner_target_rr: float = 0.0,
    max_target_rr: float = 0.0,
    runner_min_confidence: float = 0.0,
) -> LevelRepairResult:
    """Return a repaired decision or a reject when levels cannot be repaired."""
    if decision.action != "enter":
        return LevelRepairResult(True, decision, False, [], {})

    side = decision.side.lower()
    entry = float(decision.entry_price or 0.0)
    stop = float(decision.stop_price or 0.0)
    target = float(decision.target_price or 0.0)
    if side not in ("long", "short") or entry <= 0 or stop <= 0 or target <= 0:
        return LevelRepairResult(False, decision, False, ["level repair blocked: invalid entry/stop/target"], {})

    noise = coin_data.get("noise_state") or {}
    floor = float(noise.get("dynamic_stop_floor") or 0.0)
    atr14 = float(noise.get("atr14") or getattr(coin_data.get("of"), "atr_14", 0.0) or 0.0)
    required_stop = floor * float(min_stop_noise_multiple) if floor > 0 else 0.0
    levels = _structural_levels(coin_data)
    tolerance = _level_tolerance(entry, floor, coin_data)

    reasons: list[str] = []
    changed = False
    target_mode = _target_mode(decision)
    repair_kind = target_mode or "structural"

    stop_distance = abs(entry - stop)
    if not _level_order_valid(side, entry, stop, target):
        return LevelRepairResult(False, decision, False, ["level repair blocked: invalid directional level order"], {})

    if required_stop > 0 and stop_distance < required_stop:
        repaired_stop = _nearest_stop(side, entry, required_stop, levels)
        if repaired_stop is None:
            return LevelRepairResult(
                False,
                decision,
                False,
                [f"level repair blocked: no structural stop beyond noise floor {required_stop:.8g}"],
                _metrics(entry, stop, target, required_stop, floor, tolerance, levels),
            )
        reasons.append(f"stop repaired {stop:.8g}->{repaired_stop:.8g} using structural level beyond noise")
        stop = repaired_stop
        changed = True

    stop_distance = abs(entry - stop)
    fee_px = entry * (float(fee_bps) * 2.0 / 10000.0)
    min_reward = stop_distance * float(min_rr) + fee_px
    target_guide = 0.0 if target_mode == "reaction" else max(
        entry * float(min_target_pct or 0.0),
        atr14 * float(min_target_atr_multiple or 0.0),
    )
    required_reward = max(min_reward, target_guide)
    reward_distance = abs(target - entry)
    target_warning = _has_target_warning(decision)
    target_level_names = _nearest_level_names(target, levels, tolerance)
    target_structural = bool(target_level_names)
    target_preferred = (not prefer_htf_targets) or any(_is_preferred_target_name(name) for name in target_level_names)
    target_valid = (
        _target_direction_valid(side, entry, target)
        and reward_distance >= required_reward
        and not target_warning
    )
    use_runner = _should_use_runner(
        decision,
        side=side,
        coin_data=coin_data,
        target_warning=target_warning,
        runner_target_rr=runner_target_rr,
        runner_min_confidence=runner_min_confidence,
    )
    if target_valid and target_mode != "reaction" and prefer_htf_targets and not target_preferred:
        preferred_target = _nearest_target(side, entry, required_reward, levels, prefer_htf_targets=True)
        if preferred_target is not None:
            reasons.append(f"target upgraded {target:.8g}->{preferred_target:.8g} to higher-timeframe/value level")
            target = preferred_target
            reward_distance = abs(target - entry)
            changed = True

    if (
        target_valid
        and runner_target_rr > 0
        and stop_distance > 0
        and reward_distance / stop_distance > float(runner_target_rr)
        and not _strong_runner_context(side, coin_data)
    ):
        closer_target = _nearest_target(side, entry, required_reward, levels, prefer_htf_targets=prefer_htf_targets)
        if closer_target is not None and abs(closer_target - entry) < reward_distance:
            reasons.append(
                f"target tightened {target:.8g}->{closer_target:.8g} to reachable structural level"
            )
            target = closer_target
            reward_distance = abs(target - entry)
            changed = True

    if not target_valid:
        repaired_target = None
        if use_runner:
            repaired_target = _runner_target(
                side,
                entry,
                required_reward,
                stop_distance,
                levels,
                max_target_rr=float(max_target_rr or 0.0),
                prefer_htf_targets=prefer_htf_targets,
            )
        if repaired_target is None:
            repaired_target = _nearest_target(
                side,
                entry,
                required_reward,
                levels,
                prefer_htf_targets=(prefer_htf_targets and target_mode != "reaction"),
            )
        if repaired_target is None and target_mode == "reaction":
            repaired_target = _projected_reaction_target(side, entry, required_reward, max_target_rr, stop_distance)
        if repaired_target is None:
            blockers = []
            if reward_distance < required_reward:
                blockers.append(f"target reward {reward_distance:.8g} < required {required_reward:.8g}")
            if not target_structural:
                blockers.append("target is not near provided structural levels")
            if target_warning:
                blockers.append("LLM analysis says target/R:R is insufficient or borderline")
            if prefer_htf_targets and target_structural and not target_preferred:
                blockers.append("no farther higher-timeframe/value target found")
            return LevelRepairResult(
                False,
                decision,
                False,
                ["level repair blocked: " + ", ".join(blockers or ["no valid structural target"])],
                _metrics(entry, stop, target, required_stop, floor, tolerance, levels, required_reward),
            )
        repair_kind = (
            "projected reaction target"
            if target_mode == "reaction" and not _is_near_level(repaired_target, levels, tolerance)
            else "structural runner level" if use_runner
            else "next structural level"
        )
        reasons.append(f"target repaired {target:.8g}->{repaired_target:.8g} using {repair_kind}")
        target = repaired_target
        changed = True

    if not changed:
        return LevelRepairResult(
            True,
            decision,
            False,
            [],
            _metrics(entry, stop, target, required_stop, floor, tolerance, levels, required_reward),
        )

    repaired = replace(
        decision,
        stop_price=stop,
        target_price=target,
        evidence=[*decision.evidence, *reasons],
        reason=f"{decision.reason} | level_repair: {'; '.join(reasons)}",
        level_reasoning={
            **(decision.level_reasoning or {}),
            "stop": f"{stop:.8g} structural invalidation/noise-repaired level",
            "target": f"{target:.8g} final {target_mode or 'structural'} target repaired from market levels",
        },
        exit_plan={
            **(decision.exit_plan or {}),
            "stop_invalidation_level": f"{stop:.8g} structural invalidation/noise-repaired level",
            "target_level": f"{target:.8g} final {target_mode or 'structural'} target repaired from market levels",
            "runner_target_level": f"{target:.8g} final target" if target_mode == "runner" else "",
            "target_mode": target_mode or "runner",
            "target_reason": f"target repaired using {repair_kind} with sufficient reward after fees",
            "full_move_plan": (
                "single-TP execution uses a reachable reaction target"
                if target_mode == "reaction"
                else "single-TP execution uses final runner target instead of nearby scalp target"
            ),
            "rr_after_noise_stop": f"{abs(target - entry) / max(abs(entry - stop), 1e-12):.2f}R gross after repaired stop",
        },
    )
    return LevelRepairResult(
        True,
        repaired,
        True,
        reasons,
        _metrics(entry, stop, target, required_stop, floor, tolerance, levels, required_reward),
    )


def _level_order_valid(side: str, entry: float, stop: float, target: float) -> bool:
    if side == "long":
        return stop < entry < target
    return target < entry < stop


def _target_direction_valid(side: str, entry: float, target: float) -> bool:
    return target > entry if side == "long" else target < entry


def _target_mode(decision: LLMTradeDecision) -> str:
    for container in (
        decision.risk_portfolio_decision if isinstance(decision.risk_portfolio_decision, dict) else {},
        decision.exit_plan if isinstance(decision.exit_plan, dict) else {},
        decision.trader_proposal if isinstance(decision.trader_proposal, dict) else {},
    ):
        mode = str(container.get("target_mode") or "").strip().lower()
        if mode in ("reaction", "runner"):
            return mode
    return ""


def _projected_reaction_target(
    side: str,
    entry: float,
    required_reward: float,
    max_target_rr: float,
    stop_distance: float,
) -> float | None:
    reward = float(required_reward or 0.0)
    if reward <= 0:
        return None
    max_reward = float(max_target_rr or 0.0) * float(stop_distance or 0.0)
    if max_reward > 0:
        reward = min(reward, max_reward)
    if side == "long":
        return entry + reward
    projected = entry - reward
    return projected if projected > 0 else None


def _structural_levels(coin_data: dict[str, Any]) -> list[tuple[float, str]]:
    raw = coin_data.get("levels") or {}
    out: list[tuple[float, str]] = []
    for name, value in raw.items():
        _add_level(out, str(name), value)

    of = coin_data.get("of")
    if of is not None:
        for name in ("vwap", "vpoc", "vah", "val"):
            _add_level(out, name, getattr(of, name, 0.0))

    for key, label, limit in (
        ("candles_15m", "15m", 24),
        ("candles_1h", "1h", 24),
        ("candles_4h", "4h", 18),
    ):
        candles = list(coin_data.get(key) or [])[-limit:]
        if not candles:
            continue
        highs = [float(getattr(candle, "high", 0.0) or 0.0) for candle in candles]
        lows = [float(getattr(candle, "low", 0.0) or 0.0) for candle in candles]
        highs = [px for px in highs if px > 0]
        lows = [px for px in lows if px > 0]
        if highs:
            _add_level(out, f"{label}_recent_high", max(highs))
        if lows:
            _add_level(out, f"{label}_recent_low", min(lows))
        prior = candles[:-1]
        if prior:
            prior_highs = [float(getattr(candle, "high", 0.0) or 0.0) for candle in prior]
            prior_lows = [float(getattr(candle, "low", 0.0) or 0.0) for candle in prior]
            prior_highs = [px for px in prior_highs if px > 0]
            prior_lows = [px for px in prior_lows if px > 0]
            if prior_highs:
                _add_level(out, f"{label}_prior_high", max(prior_highs))
            if prior_lows:
                _add_level(out, f"{label}_prior_low", min(prior_lows))

    unique: dict[tuple[float, str], tuple[float, str]] = {}
    for px, name in out:
        unique[(round(px, 8), name)] = (px, name)
    return list(unique.values())


def _add_level(out: list[tuple[float, str]], name: str, value: Any) -> None:
    try:
        px = float(value)
    except (TypeError, ValueError):
        return
    if px > 0:
        out.append((px, name))


def _nearest_stop(side: str, entry: float, required_stop: float, levels: list[tuple[float, str]]) -> float | None:
    if side == "long":
        candidates = [px for px, _ in levels if 0 < px <= entry - required_stop]
        return max(candidates) if candidates else None
    candidates = [px for px, _ in levels if px >= entry + required_stop]
    return min(candidates) if candidates else None


def _nearest_target(
    side: str,
    entry: float,
    required_reward: float,
    levels: list[tuple[float, str]],
    *,
    prefer_htf_targets: bool = False,
) -> float | None:
    if side == "long":
        candidates = [(px, name) for px, name in levels if px >= entry + required_reward]
        if prefer_htf_targets:
            preferred = [item for item in candidates if _is_preferred_target_name(item[1])]
            if preferred:
                candidates = preferred
        return min((px for px, _ in candidates), default=None)
    candidates = [(px, name) for px, name in levels if 0 < px <= entry - required_reward]
    if prefer_htf_targets:
        preferred = [item for item in candidates if _is_preferred_target_name(item[1])]
        if preferred:
            candidates = preferred
    return max((px for px, _ in candidates), default=None)


def _runner_target(
    side: str,
    entry: float,
    min_reward: float,
    stop_distance: float,
    levels: list[tuple[float, str]],
    *,
    max_target_rr: float = 0.0,
    prefer_htf_targets: bool = True,
) -> float | None:
    if side == "long":
        candidates = [(px, name) for px, name in levels if px >= entry + min_reward]
    else:
        candidates = [(px, name) for px, name in levels if 0 < px <= entry - min_reward]
    if not candidates:
        return None

    max_reward = stop_distance * float(max_target_rr or 0.0)
    if max_reward > 0:
        capped = [(px, name) for px, name in candidates if abs(px - entry) <= max_reward]
        if capped:
            candidates = capped

    if prefer_htf_targets:
        preferred = [item for item in candidates if _is_preferred_target_name(item[1])]
        if preferred:
            candidates = preferred

    if side == "long":
        return min((px for px, _ in candidates), default=None)
    return max((px for px, _ in candidates), default=None)


def _should_use_runner(
    decision: LLMTradeDecision,
    *,
    side: str,
    coin_data: dict[str, Any],
    target_warning: bool,
    runner_target_rr: float,
    runner_min_confidence: float,
) -> bool:
    if runner_target_rr <= 0:
        return False
    if float(decision.confidence or 0.0) < float(runner_min_confidence or 0.0):
        return False
    playbook = _playbook_name(decision)
    if any(key in playbook for key in ("trend", "breakout", "breakdown", "sweep", "absorption")):
        return _strong_runner_context(side, coin_data)
    if target_warning:
        return _strong_runner_context(side, coin_data)
    return False


def _strong_runner_context(side: str, coin_data: dict[str, Any]) -> bool:
    regime = coin_data.get("regime")
    trend_15m = str(getattr(regime, "trend_15m", "") or "").lower()
    trend_1h = str(getattr(regime, "trend_1h", "") or "").lower()
    trend_4h = str(getattr(regime, "trend_4h", "") or "").lower()
    side_aligned_trend = (
        side == "long"
        and trend_15m == "up"
        and trend_1h == "up"
        and trend_4h == "up"
    ) or (
        side == "short"
        and trend_15m == "down"
        and trend_1h == "down"
        and trend_4h == "down"
    )

    ws = coin_data.get("ws_summary") or {}
    windows = ws.get("tape_windows") if isinstance(ws.get("tape_windows"), dict) else {}
    values = []
    for key in ("30s", "1m"):
        window = windows.get(key) if isinstance(windows.get(key), dict) else {}
        if float(window.get("total_usd") or 0.0) > 0:
            values.append(float(window.get("delta_ratio") or 0.0))
    if not values:
        values.append(float(ws.get("delta_ratio_30s") or 0.0))

    if side == "long":
        strong_tape = any(value >= 0.50 for value in values)
    else:
        strong_tape = any(value <= -0.50 for value in values)

    return side_aligned_trend and strong_tape


def _playbook_name(decision: LLMTradeDecision) -> str:
    match = decision.playbook_match if isinstance(decision.playbook_match, dict) else {}
    return " ".join(
        str(item or "").lower()
        for item in (
            match.get("playbook"),
            match.get("_raw_playbook"),
            decision.setup_type,
        )
    )


def _is_near_level(price: float, levels: list[tuple[float, str]], tolerance: float) -> bool:
    return bool(_nearest_level_names(price, levels, tolerance))


def _nearest_level_names(price: float, levels: list[tuple[float, str]], tolerance: float) -> list[str]:
    return [name for px, name in levels if abs(price - px) <= tolerance]


def _is_preferred_target_name(name: str) -> bool:
    normalized = name.lower()
    return any(
        key in normalized
        for key in (
            "prior_1h",
            "prior_4h",
            "recent_high",
            "recent_low",
            "prior_high",
            "prior_low",
            "last_swing",
            "vwap",
            "vpoc",
            "vah",
            "val",
        )
    )


def _level_tolerance(entry: float, floor: float, coin_data: dict[str, Any]) -> float:
    of = coin_data.get("of")
    spread_px = entry * float(getattr(of, "spread_bps", 0.0) or 0.0) / 10000.0
    return max(entry * 0.0002, floor * 0.10, spread_px * 3.0)


def _has_target_warning(decision: LLMTradeDecision) -> bool:
    fields = [
        decision.reason,
        decision.risk_assessment,
        decision.level_reasoning.get("target") if isinstance(decision.level_reasoning, dict) else "",
        decision.exit_plan.get("target_reason") if isinstance(decision.exit_plan, dict) else "",
        decision.exit_plan.get("target_level") if isinstance(decision.exit_plan, dict) else "",
        decision.exit_plan.get("rr_after_noise_stop") if isinstance(decision.exit_plan, dict) else "",
    ]
    text = " ".join(str(item or "").lower() for item in fields)
    return any(phrase in text for phrase in _TARGET_WARNING_PHRASES)


def _metrics(
    entry: float,
    stop: float,
    target: float,
    required_stop: float,
    floor: float,
    tolerance: float,
    levels: list[tuple[float, str]],
    required_reward: float | None = None,
) -> dict[str, Any]:
    stop_distance = abs(entry - stop)
    reward_distance = abs(target - entry)
    return {
        "entry": entry,
        "stop": stop,
        "target": target,
        "stop_distance": stop_distance,
        "reward_distance": reward_distance,
        "required_stop_distance": required_stop,
        "required_reward_distance": required_reward,
        "dynamic_stop_floor": floor,
        "level_tolerance": tolerance,
        "rr": reward_distance / stop_distance if stop_distance > 0 else 0.0,
        "structural_levels": [{"price": px, "name": name} for px, name in levels],
    }
