import time
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional

from PyQt5 import QtCore, QtGui, QtWidgets

try:
    import av
except Exception as exc:
    av = None
    AV_IMPORT_ERROR = exc
else:
    AV_IMPORT_ERROR = None

try:
    import cv2
except Exception as exc:
    cv2 = None
    CV2_IMPORT_ERROR = exc
else:
    CV2_IMPORT_ERROR = None

from clients._path_setup import ensure_paths

ensure_paths()

from clients.camera_client import CameraClient


class CameraStreamThread(QtCore.QThread):
    frame_received = QtCore.pyqtSignal(object, int, int)
    status_update = QtCore.pyqtSignal(dict)
    error_occurred = QtCore.pyqtSignal(str)
    keyframe_received = QtCore.pyqtSignal()

    def __init__(self, client, device_id, codec="h264", wait_for_keyframe=True):
        super().__init__()
        self.client = client
        self.device_id = device_id
        self.codec_name = codec
        self.wait_for_keyframe = wait_for_keyframe
        self.streaming = False
        self.decoder = None
        self.decoder_error = None
        self.frame_count = 0
        self.fps_counter = 0
        self.fps_time = time.time()
        self.keyframe_count = 0
        self._init_decoder()

    def _init_decoder(self):
        if av is None:
            self.decoder = None
            self.decoder_error = f"PyAV unavailable: {AV_IMPORT_ERROR}"
            return
        try:
            self.decoder = av.CodecContext.create(self.codec_name, "r")
            self.decoder.thread_count = 1
            if hasattr(self.decoder, "flags"):
                try:
                    self.decoder.flags |= av.codec.context.AV_CODEC_FLAG_LOW_DELAY
                except Exception:
                    pass
        except Exception as exc:
            self.decoder = None
            self.decoder_error = f"Decoder init failed: {exc}"

    def _decode(self, frame_data):
        if self.decoder is None or not frame_data:
            return None

        packet = av.Packet(frame_data)
        frames = self.decoder.decode(packet)
        if not frames:
            return None

        return frames[0].to_ndarray(format="rgb24")

    def _start_camera(self):
        response = self.client.start_camera(self.device_id)
        if not response.header.success:
            self.error_occurred.emit(response.header.error_message or "StartCamera failed")
            return False
        return True

    def _stop_camera(self):
        try:
            self.client.stop_camera(self.device_id)
        except Exception:
            pass

    def run(self):
        if self.decoder is None:
            if self.decoder_error:
                self.error_occurred.emit(self.decoder_error)
            return

        try:
            if not self._start_camera():
                return

            self.streaming = True
            self.frame_count = 0
            self.fps_counter = 0
            self.fps_time = time.time()
            self.keyframe_count = 0
            keyframe_seen = False

            responses = self.client.get_rgb_image_stream(
                device_id=self.device_id,
                stop_requested=lambda: not self.streaming,
            )

            for response in responses:
                if not self.streaming:
                    break

                if not response.header.success:
                    raise RuntimeError(response.header.error_message or "GetRGBImageStream failed")

                frame_data = response.color_frame.data
                is_keyframe = response.color_frame.is_key_frame

                if self.wait_for_keyframe and not keyframe_seen and not is_keyframe:
                    continue

                if is_keyframe and not keyframe_seen:
                    keyframe_seen = True
                    self.keyframe_count += 1
                    self.keyframe_received.emit()

                image = self._decode(frame_data)
                if image is None:
                    continue

                self.fps_counter += 1
                self.frame_count += 1

                now = time.time()
                if now - self.fps_time >= 1.0:
                    self.status_update.emit(
                        {
                            "fps": self.fps_counter,
                            "frames": self.frame_count,
                            "keyframes": self.keyframe_count,
                        }
                    )
                    self.fps_counter = 0
                    self.fps_time = now

                height, width = image.shape[:2]
                self.frame_received.emit(image, width, height)
        except Exception as exc:
            self.error_occurred.emit(f"Stream error: {exc}")
        finally:
            self.streaming = False
            self._stop_camera()

    def stop(self):
        self.streaming = False


class CameraPage(QtWidgets.QWidget):
    CODEC_OPTIONS = {
        "H.264 (AVC)": "h264",
        "H.265 (HEVC)": "hevc",
    }

    def __init__(
        self,
        address_provider: Optional[Callable[[], str]] = None,
        default_device_id: str = "right_hand_cam",
        parent=None,
    ):
        super().__init__(parent)
        self.address_provider = address_provider
        self.connected = False
        self.streaming = False
        self.recording = False
        self.stream_client = None
        self.stream_thread = None
        self.video_writer = None
        self.current_frame = None
        self.frame_width = 0
        self.frame_height = 0
        self.record_path = Path("./recordings")
        self.record_path.mkdir(exist_ok=True)

        self._create_ui(default_device_id)

        self.display_timer = QtCore.QTimer(self)
        self.display_timer.timeout.connect(self._update_display)
        self.display_timer.start(30)

    def _create_ui(self, default_device_id: str):
        layout = QtWidgets.QVBoxLayout(self)

        conn_group = QtWidgets.QGroupBox("Camera")
        conn_layout = QtWidgets.QGridLayout(conn_group)

        conn_layout.addWidget(QtWidgets.QLabel("Device ID:"), 0, 0)
        self.device_id = QtWidgets.QLineEdit(default_device_id)
        conn_layout.addWidget(self.device_id, 0, 1)

        conn_layout.addWidget(QtWidgets.QLabel("Codec:"), 0, 2)
        self.codec_combo = QtWidgets.QComboBox()
        self.codec_combo.addItems(list(self.CODEC_OPTIONS))
        self.codec_combo.setCurrentText("H.264 (AVC)")
        conn_layout.addWidget(self.codec_combo, 0, 3)

        self.wait_keyframe = QtWidgets.QCheckBox("Wait for first keyframe")
        self.wait_keyframe.setChecked(True)
        conn_layout.addWidget(self.wait_keyframe, 0, 4, 1, 2)

        if self.address_provider is None:
            conn_layout.addWidget(QtWidgets.QLabel("Server IP:"), 1, 0)
            self.server_ip = QtWidgets.QLineEdit("192.168.0.222")
            conn_layout.addWidget(self.server_ip, 1, 1)

            conn_layout.addWidget(QtWidgets.QLabel("Port:"), 1, 2)
            self.server_port = QtWidgets.QLineEdit("50052")
            self.server_port.setMaximumWidth(100)
            conn_layout.addWidget(self.server_port, 1, 3)
        else:
            conn_layout.addWidget(QtWidgets.QLabel("Server:"), 1, 0)
            self.address_label = QtWidgets.QLabel("")
            self.address_label.setStyleSheet("font-weight: bold;")
            conn_layout.addWidget(self.address_label, 1, 1, 1, 3)

        conn_layout.addWidget(QtWidgets.QLabel("Timeout (s):"), 1, 4)
        self.timeout_spin = QtWidgets.QSpinBox()
        self.timeout_spin.setRange(1, 10)
        self.timeout_spin.setValue(3)
        conn_layout.addWidget(self.timeout_spin, 1, 5)

        conn_layout.addWidget(QtWidgets.QLabel("Record Dir:"), 2, 0)
        self.record_dir = QtWidgets.QLineEdit(str(self.record_path))
        conn_layout.addWidget(self.record_dir, 2, 1, 1, 4)

        self.browse_btn = QtWidgets.QPushButton("Browse")
        self.browse_btn.clicked.connect(self._browse_dir)
        conn_layout.addWidget(self.browse_btn, 2, 5)

        button_layout = QtWidgets.QHBoxLayout()
        self.connect_btn = QtWidgets.QPushButton("Connect")
        self.connect_btn.clicked.connect(self._connect)
        button_layout.addWidget(self.connect_btn)

        self.preview_btn = QtWidgets.QPushButton("Preview")
        self.preview_btn.clicked.connect(self._toggle_preview)
        self.preview_btn.setEnabled(False)
        button_layout.addWidget(self.preview_btn)

        self.record_btn = QtWidgets.QPushButton("Record")
        self.record_btn.clicked.connect(self._toggle_recording)
        self.record_btn.setEnabled(False)
        button_layout.addWidget(self.record_btn)

        self.disconnect_btn = QtWidgets.QPushButton("Disconnect")
        self.disconnect_btn.clicked.connect(self._disconnect)
        self.disconnect_btn.setEnabled(False)
        button_layout.addWidget(self.disconnect_btn)
        button_layout.addStretch()
        conn_layout.addLayout(button_layout, 3, 0, 1, 6)

        layout.addWidget(conn_group)

        status_group = QtWidgets.QGroupBox("Stream Status")
        status_layout = QtWidgets.QHBoxLayout(status_group)

        self.conn_status = QtWidgets.QLabel("Disconnected")
        self.conn_status.setStyleSheet("color: red;")
        status_layout.addWidget(self.conn_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.codec_status = QtWidgets.QLabel("Codec: H.264 (AVC)")
        status_layout.addWidget(self.codec_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.fps_status = QtWidgets.QLabel("FPS: 0")
        status_layout.addWidget(self.fps_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.frame_status = QtWidgets.QLabel("Frames: 0")
        status_layout.addWidget(self.frame_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.resolution_status = QtWidgets.QLabel("Resolution: -")
        status_layout.addWidget(self.resolution_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.keyframe_status = QtWidgets.QLabel("Keyframe: waiting")
        self.keyframe_status.setStyleSheet("color: orange;")
        status_layout.addWidget(self.keyframe_status)

        status_layout.addWidget(QtWidgets.QLabel("|"))
        self.record_status = QtWidgets.QLabel("Record: idle")
        self.record_status.setStyleSheet("color: gray;")
        status_layout.addWidget(self.record_status)
        status_layout.addStretch()

        layout.addWidget(status_group)

        video_group = QtWidgets.QGroupBox("Preview")
        video_layout = QtWidgets.QVBoxLayout(video_group)

        self.video_label = QtWidgets.QLabel()
        self.video_label.setAlignment(QtCore.Qt.AlignCenter)
        self.video_label.setMinimumSize(640, 480)
        self.video_label.setStyleSheet("background-color: black; border: 1px solid gray;")
        video_layout.addWidget(self.video_label)

        layout.addWidget(video_group, 1)

        info_frame = QtWidgets.QFrame()
        info_frame.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken)
        info_layout = QtWidgets.QHBoxLayout(info_frame)
        info_label = QtWidgets.QLabel(
            "Connect to the camera service, start preview, then optionally record decoded frames."
        )
        info_label.setStyleSheet("color: gray; padding: 5px;")
        info_layout.addWidget(info_label)
        layout.addWidget(info_frame)

        self.status_bar = QtWidgets.QStatusBar()
        self.status_bar.showMessage("Idle")
        layout.addWidget(self.status_bar)

    def refresh_address_hint(self):
        if self.address_provider is None or not hasattr(self, "address_label"):
            return
        try:
            self.address_label.setText(self.address_provider())
        except Exception:
            self.address_label.setText("(app settings unavailable)")

    def _browse_dir(self):
        directory = QtWidgets.QFileDialog.getExistingDirectory(
            self, "Select record directory", self.record_dir.text()
        )
        if directory:
            self.record_dir.setText(directory)
            self.record_path = Path(directory)
            self.record_path.mkdir(exist_ok=True)

    def _get_server_addr(self):
        if self.address_provider is not None:
            return self.address_provider()
        return f"{self.server_ip.text().strip()}:{self.server_port.text().strip()}"

    def _get_codec(self):
        return self.CODEC_OPTIONS[self.codec_combo.currentText()]

    def _connect(self):
        if self.connected:
            return

        self.refresh_address_hint()
        self.status_bar.showMessage("Connecting to camera service...")
        self.codec_status.setText(f"Codec: {self.codec_combo.currentText()}")

        try:
            self.stream_client = CameraClient(
                address=self._get_server_addr(),
                timeout=self.timeout_spin.value(),
            )
        except Exception as exc:
            self.stream_client = None
            self._on_error(f"Connect failed: {exc}")
            return

        self.connected = True
        self.conn_status.setText("Connected")
        self.conn_status.setStyleSheet("color: green;")
        self.connect_btn.setEnabled(False)
        self.preview_btn.setEnabled(True)
        self.disconnect_btn.setEnabled(True)
        self.codec_combo.setEnabled(False)
        self.wait_keyframe.setEnabled(False)
        self.timeout_spin.setEnabled(False)
        if self.address_provider is None:
            self.server_ip.setEnabled(False)
            self.server_port.setEnabled(False)
        self.status_bar.showMessage("Connected to camera service", 3000)

    def _disconnect(self):
        if self.streaming:
            self._stop_preview()

        if self.recording:
            self._stop_recording()

        if self.stream_client:
            self.stream_client.close()
            self.stream_client = None

        self.connected = False
        self.stream_thread = None
        self.conn_status.setText("Disconnected")
        self.conn_status.setStyleSheet("color: red;")
        self.connect_btn.setEnabled(True)
        self.preview_btn.setEnabled(False)
        self.preview_btn.setText("Preview")
        self.record_btn.setEnabled(False)
        self.record_btn.setText("Record")
        self.disconnect_btn.setEnabled(False)
        self.codec_combo.setEnabled(True)
        self.wait_keyframe.setEnabled(True)
        self.timeout_spin.setEnabled(True)
        if self.address_provider is None:
            self.server_ip.setEnabled(True)
            self.server_port.setEnabled(True)
        self._clear_preview()
        self._reset_status_labels()
        self.status_bar.showMessage("Disconnected", 3000)

    def _toggle_preview(self):
        if self.streaming:
            self._stop_preview()
        else:
            self._start_preview()

    def _start_preview(self):
        if not self.connected or self.stream_client is None or self.streaming:
            return

        self.stream_thread = CameraStreamThread(
            client=self.stream_client,
            device_id=self.device_id.text().strip(),
            codec=self._get_codec(),
            wait_for_keyframe=self.wait_keyframe.isChecked(),
        )
        self.stream_thread.frame_received.connect(self._on_frame_received)
        self.stream_thread.status_update.connect(self._on_status_update)
        self.stream_thread.error_occurred.connect(self._on_error)
        self.stream_thread.keyframe_received.connect(self._on_keyframe_received)
        self.stream_thread.finished.connect(self._on_stream_finished)

        self.streaming = True
        self.preview_btn.setText("Stop")
        self.record_btn.setEnabled(True)

        if self.wait_keyframe.isChecked():
            self.keyframe_status.setText("Keyframe: waiting")
            self.keyframe_status.setStyleSheet("color: orange;")
        else:
            self.keyframe_status.setText("Keyframe: not required")
            self.keyframe_status.setStyleSheet("color: gray;")

        self.status_bar.showMessage("Starting preview...")
        self.stream_thread.start()

    def _stop_preview(self):
        if self.recording:
            self._stop_recording()

        thread = self.stream_thread
        if thread is not None:
            thread.stop()
            thread.wait(2000)
            if self.stream_thread is thread:
                self._on_stream_finished()
        else:
            self._on_stream_finished()

    def _on_stream_finished(self):
        if self.recording:
            self._stop_recording()
        self.streaming = False
        self.stream_thread = None
        self.preview_btn.setText("Preview")
        self.record_btn.setEnabled(False)
        self.keyframe_status.setText("Keyframe: waiting")
        self.keyframe_status.setStyleSheet("color: orange;")
        self.status_bar.showMessage("Preview stopped", 3000)

    def _toggle_recording(self):
        if self.recording:
            self._stop_recording()
        else:
            self._start_recording()

    def _start_recording(self):
        if not self.streaming:
            QtWidgets.QMessageBox.warning(self, "Warning", "Start preview before recording.")
            return
        if cv2 is None:
            QtWidgets.QMessageBox.critical(self, "Error", f"OpenCV unavailable: {CV2_IMPORT_ERROR}")
            return

        self.record_path = Path(self.record_dir.text())
        self.record_path.mkdir(parents=True, exist_ok=True)

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        codec_name = self.codec_combo.currentText().replace(" ", "_")
        filename = self.record_path / f"rec_{self.device_id.text()}_{codec_name}_{timestamp}.mp4"

        if self.current_frame is not None:
            height, width = self.current_frame.shape[:2]
        else:
            width, height = 640, 480

        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        self.video_writer = cv2.VideoWriter(str(filename), fourcc, 30.0, (width, height))

        if not self.video_writer.isOpened():
            self.video_writer = None
            QtWidgets.QMessageBox.critical(self, "Error", "Failed to create the output video file.")
            return

        self.recording = True
        self.record_btn.setText("Stop Recording")
        self.record_status.setText("Record: active")
        self.record_status.setStyleSheet("color: red;")
        self.status_bar.showMessage(f"Recording to {filename.name}", 3000)

    def _stop_recording(self):
        if self.video_writer:
            self.video_writer.release()
            self.video_writer = None

        self.recording = False
        self.record_btn.setText("Record")
        self.record_status.setText("Record: idle")
        self.record_status.setStyleSheet("color: gray;")
        self.status_bar.showMessage("Recording stopped", 3000)

    def _on_frame_received(self, frame, width, height):
        self.current_frame = frame
        self.frame_width = width
        self.frame_height = height

        if self.recording and self.video_writer and cv2 is not None:
            self.video_writer.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))

    def _update_display(self):
        if self.current_frame is None:
            return

        bytes_per_line = 3 * self.frame_width
        qimage = QtGui.QImage(
            self.current_frame.data,
            self.frame_width,
            self.frame_height,
            bytes_per_line,
            QtGui.QImage.Format_RGB888,
        )

        pixmap = QtGui.QPixmap.fromImage(qimage).scaled(
            self.video_label.size(),
            QtCore.Qt.KeepAspectRatio,
            QtCore.Qt.SmoothTransformation,
        )
        self.video_label.setPixmap(pixmap)
        self.resolution_status.setText(f"Resolution: {self.frame_width}x{self.frame_height}")

    def _on_status_update(self, stats):
        self.fps_status.setText(f"FPS: {stats['fps']}")
        self.frame_status.setText(f"Frames: {stats['frames']}")

    def _on_keyframe_received(self):
        if self.wait_keyframe.isChecked():
            self.keyframe_status.setText("Keyframe: received")
            self.keyframe_status.setStyleSheet("color: green;")

    def _on_error(self, message):
        QtWidgets.QMessageBox.critical(self, "Error", message)
        self.status_bar.showMessage(message, 5000)

    def _clear_preview(self):
        self.video_label.clear()
        self.current_frame = None
        self.frame_width = 0
        self.frame_height = 0

    def _reset_status_labels(self):
        self.fps_status.setText("FPS: 0")
        self.frame_status.setText("Frames: 0")
        self.resolution_status.setText("Resolution: -")
        self.keyframe_status.setText("Keyframe: waiting")
        self.keyframe_status.setStyleSheet("color: orange;")
        self.record_status.setText("Record: idle")
        self.record_status.setStyleSheet("color: gray;")

    def shutdown(self):
        if self.streaming:
            self._stop_preview()
        if self.recording:
            self._stop_recording()
        if self.connected:
            self._disconnect()


class CameraWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Camera Stream Player")
        self.resize(1000, 800)
        self.camera_page = CameraPage(parent=self)
        self.setCentralWidget(self.camera_page)

    def closeEvent(self, event):
        self.camera_page.shutdown()
        super().closeEvent(event)
