import json
import time
from pathlib import Path

import cv2
import numpy as np

from config import *
from helpers import box_center, clamp, clip_box


def _curve_command(value, deadzone, expo, gain):
    v = float(np.clip(value, -1.0, 1.0))
    av = abs(v)
    if av <= deadzone:
        return 0.0
    t = (av - deadzone) / max(1e-6, 1.0 - deadzone)
    out = min(1.0, float(gain) * (t ** float(expo)))
    return out if v >= 0.0 else -out


class ScreenGuidanceController:
    def __init__(self):
        self.enabled = bool(GUIDANCE_ENABLE)
        self.export_enable = bool(self.enabled and GUIDANCE_EXPORT_ENABLE)
        self.export_every = int(max(1, GUIDANCE_EXPORT_EVERY))
        self.export_path = Path(GUIDANCE_EXPORT_PATH)
        if self.export_enable:
            self.export_path.parent.mkdir(parents=True, exist_ok=True)

        self.cmd_keep = float(clamp(GUIDANCE_CMD_SMOOTH, 0.0, 0.97))
        self.point_keep = float(clamp(GUIDANCE_POINT_SMOOTH, 0.0, 0.97))
        self.last_export_frame = -10**9
        self.export_retry_at = 0.0
        self.last_export_warn_ts = 0.0
        self.smoothed_cmd = np.zeros(2, dtype=np.float32)
        self.smoothed_point = None

    def status_line(self):
        if not self.enabled:
            return "Guidance disabled"
        if self.export_enable:
            return f"Guidance ready: export={self.export_path}"
        return "Guidance ready"

    def reset_for_target_switch(self, aim_point, cmd_damp=0.0):
        aim = np.array(aim_point, dtype=np.float32)
        self.smoothed_point = aim.copy()
        damp = float(clamp(cmd_damp, 0.0, 1.0))
        self.smoothed_cmd = (self.smoothed_cmd * damp).astype(np.float32)

    def idle_state(self):
        return {
            "active": False,
            "status": "SEARCH",
            "frame_id": -1,
            "target_id": None,
            "frame_w": None,
            "frame_h": None,
            "aim_x": None,
            "aim_y": None,
            "box_w": None,
            "box_h": None,
            "error_x": 0.0,
            "error_y": 0.0,
            "cmd_x": 0.0,
            "cmd_y": 0.0,
            "steer_x": 0.0,
            "steer_y": 0.0,
            "look_dx": 0.0,
            "look_dy": 0.0,
            "confidence": 0.0,
            "on_target": False,
        }

    def update(
        self,
        frame_id,
        frame_w,
        frame_h,
        confirmed,
        locked_box,
        pred_box,
        override_box,
        target_id,
        target_track_score,
        klt_valid,
        klt_quality,
        miss_streak,
        vx,
        vy,
    ):
        state = self.idle_state()
        state["frame_id"] = int(frame_id)
        state["target_id"] = None if target_id is None else int(target_id)
        state["frame_w"] = int(frame_w)
        state["frame_h"] = int(frame_h)

        if not self.enabled:
            return state

        guide_box = None
        if confirmed and override_box is not None:
            guide_box = np.array(override_box, dtype=np.float32)
        elif confirmed and locked_box is not None:
            guide_box = np.array(locked_box, dtype=np.float32)
        elif confirmed and pred_box is not None:
            guide_box = np.array(pred_box, dtype=np.float32)

        if guide_box is None:
            self.smoothed_cmd *= 0.60
            state["cmd_x"] = float(self.smoothed_cmd[0])
            state["cmd_y"] = float(self.smoothed_cmd[1])
            state["steer_x"] = float(self.smoothed_cmd[0])
            state["steer_y"] = float(self.smoothed_cmd[1])
            state["look_dx"] = float(self.smoothed_cmd[0] * float(GUIDANCE_LOOK_MAX_STEP_PX))
            state["look_dy"] = float(self.smoothed_cmd[1] * float(GUIDANCE_LOOK_MAX_STEP_PX))
            self._maybe_export(state)
            return state

        guide_box = clip_box(guide_box, frame_w, frame_h)
        center = box_center(guide_box).astype(np.float32)
        box_w = float(max(0.0, guide_box[2] - guide_box[0]))
        box_h = float(max(0.0, guide_box[3] - guide_box[1]))

        lead = np.array([float(vx), float(vy)], dtype=np.float32) * float(GUIDANCE_LEAD_SEC)
        lead_norm = float(np.linalg.norm(lead))
        if lead_norm > float(GUIDANCE_MAX_LEAD_PX):
            lead *= float(GUIDANCE_MAX_LEAD_PX) / max(1e-6, lead_norm)

        aim = center + lead
        aim[1] += float(GUIDANCE_BOX_BIAS_Y) * max(2.0, float(guide_box[3] - guide_box[1]))
        aim[0] = float(clamp(aim[0], 0.0, float(frame_w - 1)))
        aim[1] = float(clamp(aim[1], 0.0, float(frame_h - 1)))

        if self.smoothed_point is None:
            self.smoothed_point = aim.copy()
        else:
            self.smoothed_point = (
                (self.point_keep * self.smoothed_point)
                + ((1.0 - self.point_keep) * aim)
            ).astype(np.float32)

        screen_center = np.array([0.5 * float(frame_w), 0.5 * float(frame_h)], dtype=np.float32)
        error_px = self.smoothed_point - screen_center
        error_norm = np.array(
            [
                float(error_px[0] / max(1.0, 0.5 * float(frame_w))),
                float(error_px[1] / max(1.0, 0.5 * float(frame_h))),
            ],
            dtype=np.float32,
        )
        error_norm = np.clip(error_norm, -1.0, 1.0)

        raw_cmd = np.array(
            [
                _curve_command(error_norm[0], GUIDANCE_DEADZONE_NORM, GUIDANCE_EXPO, GUIDANCE_GAIN_X),
                _curve_command(error_norm[1], GUIDANCE_DEADZONE_NORM, GUIDANCE_EXPO, GUIDANCE_GAIN_Y),
            ],
            dtype=np.float32,
        )
        self.smoothed_cmd = (
            (self.cmd_keep * self.smoothed_cmd)
            + ((1.0 - self.cmd_keep) * raw_cmd)
        ).astype(np.float32)

        confidence = 0.0
        confidence += 0.40 if confirmed else 0.0
        confidence += 0.15 if locked_box is not None else 0.05

        # KLT теперь главный источник доверия (klt_quality ≈ 0.93)
        if klt_valid:
            confidence += 0.30 * float(clamp(klt_quality, 0.0, 1.0))
        else:
            confidence += 0.05 * float(clamp(klt_quality, 0.0, 1.0))

        # ByteTrack score — второстепенный
        if target_track_score is not None:
            confidence += 0.10 * float(clamp(target_track_score, 0.0, 1.0))

        # Мягче штраф за miss (YOLO слабый, это временно)
        confidence -= 0.025 * float(clamp(miss_streak, 0, 10))

        confidence = float(clamp(confidence, 0.0, 1.0))

        err_mag = float(np.linalg.norm(error_norm))
        on_target = bool(
            confirmed
            and (confidence >= float(GUIDANCE_CONF_MIN))
            and (err_mag <= float(GUIDANCE_ON_TARGET_NORM))
        )

        status = "SEARCH"
        if confirmed and override_box is not None and miss_streak > 0:
            status = "REACQ"
        elif confirmed and miss_streak > 0:
            status = "HOLD"
        elif confirmed:
            status = "LOCK"

        state.update(
            {
                "active": bool(confirmed),
                "status": status,
                "aim_x": float(self.smoothed_point[0]),
                "aim_y": float(self.smoothed_point[1]),
                "box_w": box_w,
                "box_h": box_h,
                "error_x": float(error_norm[0]),
                "error_y": float(error_norm[1]),
                "cmd_x": float(self.smoothed_cmd[0]),
                "cmd_y": float(self.smoothed_cmd[1]),
                "steer_x": float(self.smoothed_cmd[0]),
                "steer_y": float(self.smoothed_cmd[1]),
                "look_dx": float(self.smoothed_cmd[0] * float(GUIDANCE_LOOK_MAX_STEP_PX)),
                "look_dy": float(self.smoothed_cmd[1] * float(GUIDANCE_LOOK_MAX_STEP_PX)),
                "confidence": confidence,
                "on_target": on_target,
            }
        )
        self._maybe_export(state)
        return state

    def draw_overlay(self, frame_bgr, state, sx, sy):
        if (not self.enabled) or (not DRAW_GUIDANCE):
            return

        frame_h, frame_w = frame_bgr.shape[:2]
        cx = int(0.5 * frame_w)
        cy = int(0.5 * frame_h)

        color = (0, 220, 255) if state.get("active", False) else (120, 120, 120)
        cv2.drawMarker(frame_bgr, (cx, cy), color, markerType=cv2.MARKER_CROSS, markerSize=18, thickness=1)

        aim_x = state.get("aim_x", None)
        aim_y = state.get("aim_y", None)
        if aim_x is not None and aim_y is not None:
            ax = int(float(aim_x) / max(1e-6, float(sx)))
            ay = int(float(aim_y) / max(1e-6, float(sy)))
            cv2.circle(frame_bgr, (ax, ay), 8, color, 2)
            cv2.line(frame_bgr, (cx, cy), (ax, ay), color, 1, cv2.LINE_AA)
            cv2.putText(
                frame_bgr,
                f"G {state['status']} steer=({state['steer_x']:+.2f},{state['steer_y']:+.2f}) conf={state['confidence']:.2f}",
                (20, 165),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.60,
                color,
                2,
            )

    def _maybe_export(self, state):
        frame_id = int(state["frame_id"])
        now = time.monotonic()
        if (not self.export_enable) or ((frame_id - self.last_export_frame) < self.export_every) or (now < self.export_retry_at):
            return

        payload = {
            "frame_id": frame_id,
            "active": bool(state["active"]),
            "status": str(state["status"]),
            "target_id": state["target_id"],
            "frame_w": state["frame_w"],
            "frame_h": state["frame_h"],
            "aim_x": state["aim_x"],
            "aim_y": state["aim_y"],
            "box_w": state["box_w"],
            "box_h": state["box_h"],
            "error_x": float(state["error_x"]),
            "error_y": float(state["error_y"]),
            "cmd_x": float(state["cmd_x"]),
            "cmd_y": float(state["cmd_y"]),
            "steer_x": float(state["steer_x"]),
            "steer_y": float(state["steer_y"]),
            "look_dx": float(state["look_dx"]),
            "look_dy": float(state["look_dy"]),
            "confidence": float(state["confidence"]),
            "on_target": bool(state["on_target"]),
        }
        tmp_path = self.export_path.with_suffix(self.export_path.suffix + ".tmp")
        try:
            with tmp_path.open("w", encoding="utf-8") as f:
                json.dump(payload, f, ensure_ascii=True, indent=2)
            self._replace_export_file(tmp_path)
        except PermissionError as exc:
            self.export_retry_at = time.monotonic() + 0.25
            self._warn_export_error(frame_id, exc)
            return
        except OSError as exc:
            self.export_retry_at = time.monotonic() + 0.50
            self._warn_export_error(frame_id, exc)
            return

        self.export_retry_at = 0.0
        self.last_export_frame = frame_id

    def _replace_export_file(self, tmp_path):
        last_err = None
        for delay_sec in (0.0, 0.01, 0.03):
            if delay_sec > 0.0:
                time.sleep(delay_sec)
            try:
                tmp_path.replace(self.export_path)
                return
            except PermissionError as exc:
                last_err = exc

        if last_err is not None:
            raise last_err

    def _warn_export_error(self, frame_id, exc):
        now = time.monotonic()
        if (now - self.last_export_warn_ts) < 2.0:
            return
        print(f"[guidance] export skipped at frame {frame_id} for {self.export_path}: {exc}")
        self.last_export_warn_ts = now
