import numpy as np
import cv2
from typing import Iterable, Optional, Dict, Tuple, List
from jobs.job_base import JobBase
from domain.type import Detection,Box
from ultralytics import YOLO


class JobDetectObjectsForCar(JobBase):
    """
    駐車場の画像から物体検出を行うジョブ

    - YOLOv8を使用して車両を検出する。
    - 検出結果はstatus.detected_objectsに格納
    """

    # モデルはクラス変数で共有(初回だけDL/ロード)
    _model: Optional[YOLO] = None
    _names: Optional[Iterable[str]] = None

    # デフォルト設定
    ALLOWED_CLASSES = {"car", "truck", "bus", "motorbike", "bicycle"}

    def __init__(
        self,
        weights: str = "yolov8n.pt",
        conf: float = 0.35,
        iou: float = 0.5,
        allowed_classes: Optional[Iterable[str]] = None,
        roi: Optional[Dict[str, float]] = None,  # {"top":0~1, "bottom":0~1, "left":0~1, "right":0~1}
    ):
        """
        Args:
            conf (float): 信頼度の閾値(物体を「正しく検知できた」と判定する最低限の確率値)
            iou (float): NMSのIoU閾値(重複する検出を抑制するための閾値)
            allowed_classes (Optional[Iterable[str]]): 検出対象とするクラスのリスト(デフォルトは車両関連のみ)
            roi (Optional[Dict[str, float]]): 関心領域(ROI)の指定。画像の一部だけを検出対象にする場合に使用。

        Notes:
            - confは0.0-1.0(例: 0.35 は 35% 以上の確信度)
                - 小さくすると → 検知数が増える(でも誤検知も増える)
                - 大きくすると → 精度が高いものだけ残る(でも見逃しも増える)
                - 駐車場で車両を数えるなら 0.3-0.5くらいが実用的。
            - iouは0.0-1.0(例: 0.5 は 50% 以上重なっていると同じ物体とみなす)
                - 小さくすると → 似た位置にある物体を別々に検知しやすい(でも誤検知も増える)
                - 大きくすると → 似た位置にある物体をまとめて検知する(でも見逃しも増える)
                - 一般的に 0.45-0.6 が推奨。駐車場のように「車が並ぶ環境」だと 0.5くらいがバランス良いです。
            - allowed_classes を指定しない場合、全クラスが検出対象になる。
                - 
            - roiは画像の一部だけを検出対象にする場合に使用。座標は0.0-1.0の割合で指定。
                - 例: {"top":0.2, "bottom":0.8, "left":0.1, "right":0.9} は画像の中央部分を指定。
                - Noneの場合、画像全体が検出対象になる。
            - キャッシュについて
                - アプリ起動中はキャッシュされる(速い)
                - アプリを終了→再起動すると再ロードが必要(数秒で終わる)
                - Ultralytics は ~/.cache/ultralytics/ に保存します。

        """
        super().__init__(name=self.__class__.__name__)
        self.description = "Detect Objects for Car Job"

        self.weights = weights
        self.conf = float(conf)
        self.iou = float(iou)
        self.allowed = set(allowed_classes) if allowed_classes else set(self.ALLOWED_CLASSES)
        self.roi = roi  # 省略可

    # ---------- 内部ユーティリティ ----------
    @classmethod
    def _ensure_model(cls, weights: str):
        if cls._model is None:
            cls._model = YOLO(weights)  # 初回は自動DL
            # names は list か dict のことがある
            cls._names = getattr(cls._model, "names", None)
        return cls._model, cls._names


    @staticmethod
    def _apply_roi(img: np.ndarray, roi: Optional[Dict[str, float]]) -> Tuple[np.ndarray, Tuple[int, int]]:
        if not roi:
            return img, (0, 0)
        h, w = img.shape[:2]
        t = int(h * roi.get("top", 0.0))
        b = int(h * (1.0 - roi.get("bottom", 0.0)))
        l = int(w * roi.get("left", 0.0))
        r = int(w * (1.0 - roi.get("right", 0.0)))
        t = max(0, min(t, h-1)); b = max(t+1, min(b, h))
        l = max(0, min(l, w-1)); r = max(l+1, min(r, w))
        return img[t:b, l:r].copy(), (l, t)


    @staticmethod
    def _offset_boxes(dets: List[Detection], off: Tuple[int, int]) -> List[Detection]:
        if off == (0, 0):
            return dets
        ox, oy = off
        out: List[Detection] = []
        for d in dets:
            b = d.box
            out.append(Detection(d.label, d.score, Box(b.x1+ox, b.y1+oy, b.x2+ox, b.y2+oy)))
        return out


    def execute(self):
        self.logger.info(f"{self.name} execute started")
        # ここにYOLOv8を使用した物体検出のコードを実装
        # 検出結果をself.status.detected_objectsに格納
        self.logger.info(f"{self.name} execute finished")
        pass  # 実装は後で追加


    @staticmethod
    def _draw(image_bgr: np.ndarray, dets: List[Detection]) -> np.ndarray:
        out = image_bgr.copy()
        for d in dets:
            x1, y1, x2, y2 = d.box.x1, d.box.y1, d.box.x2, d.box.y2
            cv2.rectangle(out, (x1, y1), (x2, y2), (0, 200, 0), 2)
            cv2.putText(out, f"{d.label} {d.score:.2f}", (x1, max(0, y1 - 6)),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 2)
        # バナー
        cv2.rectangle(out, (10, 10), (260, 52), (0, 0, 0), -1)
        cv2.putText(out, f"Count: {len(dets)}", (18, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
        return out

    # ---------- 実行本体 ----------
    def execute(self):
        self.logger.info(f"{self.name} execute started")

        # 1) 入力画像の取得（Standardize後を推奨）
        img_bgr: Optional[np.ndarray] = self.status.processed_image
        if img_bgr is None:
            # 標準化前でも original_image があれば一応通す
            img_bgr = self.status.original_image

        if img_bgr is None:
            self.logger.error("No image found in status (std_image_bgr/original_image)")
            return

        # 2) モデルの用意（初回だけロード）
        model, names = self._ensure_model(self.weights)

        # 3) ROI（任意）
        roi_img, offset = self._apply_roi(img_bgr, self.roi)

        # 4) 推論（UltralyticsはRGB想定なので[..., ::-1]でOK）
        res = model.predict(roi_img[..., ::-1], conf=self.conf, iou=self.iou, verbose=False)[0]
        names = model.model.names if hasattr(model, "model") and hasattr(model.model, "names") else model.names

        # 5) 後処理（クラスフィルタ → Detectionへ詰め替え）
        dets: List[Detection] = []
        for b in res.boxes:
            cls_id = int(b.cls[0])
            label = names[cls_id] if isinstance(names, (list, tuple)) else names.get(cls_id, str(cls_id))
            if label not in self.allowed:
                continue
            x1, y1, x2, y2 = map(int, b.xyxy[0])
            conf = float(b.conf[0])
            dets.append(Detection(label=label, score=conf, box=Box(x1, y1, x2, y2)))

        # ROIオフセットを元画像座標に戻す
        dets = self._offset_boxes(dets, offset)

        # 6) 可視化画像を生成
        overlay = self._draw(img_bgr, dets)

        # 7) statusへ保存
        self.status.detected_objects = dets
        self.status.overlay_image = overlay
        self.logger.info(f"detected={len(dets)}, conf>={self.conf}, iou={self.iou}, allowed={sorted(self.allowed)}")
        self.logger.info(f"{self.name} execute finished")