`import cv2
from deep_sort_realtime.deepsort_tracker import DeepSort
from datetime import datetime, timedelta
from Status import Status
from insightface.app import FaceAnalysis
import numpy as np
import concurrent.futures


class ObjectTracker:
    """
    A class for tracking objects using Deep Sort and updating their status in the database.

    Attributes:
        db (Database): The database instance for storing object data.
        detection_model (ObjectDetection): The object detection model.
        tracker (DeepSort): The Deep Sort tracker instance.
        object_positions (dict): Dictionary to store the positions of each object.
    """
    def __init__(self, db, detection_model):
        """
        Initializes the ObjectTracker class with the given database and detection model.

        Args:
            db (Database): The database instance.
            detection_model (ObjectDetection): The object detection model.
        """
        self.db = db
        self.detection_model = detection_model
        self.tracker = DeepSort(max_age=10, n_init=3, nms_max_overlap=1.0,
                                max_cosine_distance=0.4, nn_budget=None)
        self.object_positions = {}  # To store positions of each object
        # Initialize ArcFace for face recognition
        self.face_recognizer = FaceAnalysis(allowed_modules=['detection', 'recognition'])
        self.face_recognizer.prepare(ctx_id=0, det_size=(320, 320))
        self.face_recognition_cache = {}  # Stores obj_id: (person_name, last_recognition_frame)
        self.face_recognition_interval = 10  # Number of frames to wait before re-running face recognition
        self.frame_count = 0  # Initialize frame counter
        self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)

    def is_near(self, existing_obj, new_obj, threshold=30):
        """
        Determines if a new object is near an existing object based on a threshold.

        Args:
            existing_obj (tuple): The existing object's coordinates and size (x, y, w, h).
            new_obj (tuple): The new object's coordinates and size (x, y, w, h).
            threshold (int, optional): The distance threshold to consider.

        Returns:
            bool: True if the objects are near each other, False otherwise.
        """
        ex, ey, ew, eh = existing_obj
        nx, ny, nw, nh = new_obj
        # Check if bounding boxes are close enough
        return not (nx > ex + ew + threshold or
                    nx + nw < ex - threshold or
                    ny > ey + eh + threshold or
                    ny + nh < ey - threshold)

    def is_within_frame(self, obj_id, frame_width, frame_height, border_threshold=20, size_threshold=60):
        """
        Checks if an object is within the frame or near the borders. If the object is near a border and its size is
        small (less than size_threshold), it is considered out of bounds.

        Args:
            obj_id (int): The ID of the object.
            frame_width (int): The width of the frame.
            frame_height (int): The height of the frame.
            border_threshold (int, optional): The distance from borders to consider as 'near the border'.
            size_threshold (int, optional): The size threshold to consider the object as out of bounds if near border.

        Returns:
            bool: True if the object is within the frame, False if it's out of bounds.
        """
        positions = self.object_positions.get(obj_id, [])
        if not positions:
            return False  # No position data available

        x, y, w, h = positions[-1]  # Get the most recent position

        # Check distances from borders
        distance_left = x
        distance_right = frame_width - (x + w)
        distance_top = y
        distance_bottom = frame_height - (y + h)

        # If object is near left border
        if distance_left <= border_threshold and w <= size_threshold:
            return False  # Object is out of bounds
        # If object is near right border
        if distance_right <= border_threshold and w <= size_threshold:
            return False
        # If object is near top border
        if distance_top <= border_threshold and h <= size_threshold:
            return False
        # If object is near bottom border
        if distance_bottom <= border_threshold and h <= size_threshold:
            return False

        # If none of the above conditions met, object is within frame
        return True

    def check_for_false_positives(self, frame_width, frame_height):
        """
        Checks the database for objects that haven't been seen for more than 500 ms.
        If an object hasn't been seen for more than 500 ms and is not within frame,
        updates its status to 'Left Frame'. If detection_count < 5, deletes it.

        Args:
            frame_width (int): The width of the frame.
            frame_height (int): The height of the frame.
        """
        all_objects = self.db.fetch_all_objects()
        now = datetime.now()
        detection_timeout = now - timedelta(milliseconds=500)

        for obj in all_objects:
            obj_id = obj[0]
            class_name = obj[1]
            x1 = obj[2]
            y1 = obj[3]
            width = obj[4]
            height = obj[5]
            last_seen_str = obj[6]
            status = obj[7]
            detection_count = obj[8]

            last_seen = datetime.fromisoformat(last_seen_str)

            # If the object hasn't been seen for more than 500 ms
            if last_seen < detection_timeout:
                if detection_count < 6:
                    # Delete the object from the database
                    self.db.delete_object(obj_id)
                    # Remove from position tracking
                    if obj_id in self.object_positions:
                        del self.object_positions[obj_id]
                else:
                    # Check if the object is within the frame
                    if not self.is_within_frame(obj_id, frame_width, frame_height):
                        # Update the object's status to 'Left Frame'
                        self.db.update_status(obj_id, Status.LEFT_FRAME)
    
    def track_objects(self, img):
        self.frame_count += 1  # Update frame count
        frame_height, frame_width = img.shape[:2]
        detections = self.detection_model.detect_objects(img)
        formatted_detections = []

        # Fetch all existing objects once per frame
        existing_objs = self.db.fetch_all_objects()
        existing_objs_dict = {}  # Map class_name to list of objects
        for obj in existing_objs:
            class_name = obj[1]
            if class_name not in existing_objs_dict:
                existing_objs_dict[class_name] = []
            existing_objs_dict[class_name].append(obj)

        for class_name, x1, y1, width, height, confidence in detections:
            new_obj = (x1, y1, width, height)
            formatted_detections.append(([x1, y1, width, height], confidence, class_name))
            cv2.rectangle(img, (x1, y1), (x1 + width, y1 + height), (255, 0, 255), 3)
            cv2.putText(img, f'{class_name} {confidence:.2f}', (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)

            person_name = None  # Initialize person_name

            if class_name == 'person':
                objs_in_class = existing_objs_dict.get(class_name, [])
                preexisting_obj = False
                obj_id = None

                for existing_obj in objs_in_class:
                    db_x1 = existing_obj[2]
                    db_y1 = existing_obj[3]
                    db_width = existing_obj[4]
                    db_height = existing_obj[5]
                    last_seen = datetime.fromisoformat(existing_obj[6])
                    status = Status(existing_obj[7])

                    if self.is_near((db_x1, db_y1, db_width, db_height), new_obj):
                        preexisting_obj = True
                        obj_id = existing_obj[0]
                        self.db.update_object(obj_id, x1, y1, width, height)
                        self.object_positions[obj_id] = [(x1, y1, width, height)]
                        break

                if not preexisting_obj:
                    obj_id = self.db.insert_object(class_name, x1, y1, width, height)
                    self.object_positions[obj_id] = [(x1, y1, width, height)]
                    self.db.update_status(obj_id, Status.IN_FRAME)
                    # Perform face recognition for new object
                    self.executor.submit(self.async_face_recognition, obj_id, img, x1, y1, width, height)
                    if person_name:
                        self.db.update_person_name(obj_id, person_name)
                        self.face_recognition_cache[obj_id] = (person_name, self.frame_count)
                else:
                    last_recognition_frame = self.face_recognition_cache.get(obj_id, (None, -self.face_recognition_interval))[1]
                    if self.frame_count - last_recognition_frame >= self.face_recognition_interval:
                        person_name = self.perform_face_recognition(img, x1, y1, width, height)
                        if person_name:
                            self.db.update_person_name(obj_id, person_name)
                            self.face_recognition_cache[obj_id] = (person_name, self.frame_count)
                    else:
                        # Use cached name
                        person_name = self.face_recognition_cache.get(obj_id, (None, None))[0]

                if person_name and person_name != 'Unknown':
                    cv2.putText(img, f'Name: {person_name}', (x1, y1 - 30),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
            existing_objs = self.db.fetch_objects(class_name)
            preexisting_obj = False
            obj_id = None

            for existing_obj in existing_objs:
                db_x1 = existing_obj[2]
                db_y1 = existing_obj[3]
                db_width = existing_obj[4]
                db_height = existing_obj[5]
                last_seen = datetime.fromisoformat(existing_obj[6])
                status = Status(existing_obj[7])

                if self.is_near((db_x1, db_y1, db_width, db_height), new_obj):
                    preexisting_obj = True
                    obj_id = existing_obj[0]
                    self.db.update_object(obj_id, x1, y1, width, height)
                    if class_name == 'person':
                        self.db.update_person_name(obj_id, person_name)
                    self.object_positions[obj_id] = [(x1, y1, width, height)]
                    break

            if not preexisting_obj:
                obj_id = self.db.insert_object(class_name, x1, y1, width, height, person_name)
                self.object_positions[obj_id] = [(x1, y1, width, height)]
            else:
                self.object_positions[obj_id] = [(x1, y1, width, height)]
                self.db.update_status(obj_id, Status.IN_FRAME)

        tracks = self.tracker.update_tracks(formatted_detections, frame=img)
        self.check_for_false_positives(frame_width, frame_height)
        return img

    def perform_face_recognition(self, img, x1, y1, width, height):
        person_img = img[y1:y1+height, x1:x1+width]
        faces = self.face_recognizer.get(person_img)
        if faces:
            face = faces[0]
            face_embedding = face.embedding / np.linalg.norm(face.embedding)
            known_faces = self.db.fetch_known_faces()
            min_distance = float('inf')
            matched_name = None

            for known_face in known_faces:
                known_name = known_face[1]
                known_embedding = known_face[2] / np.linalg.norm(known_face[2])
                distance = np.linalg.norm(face_embedding - known_embedding)
                if distance < min_distance:
                    min_distance = distance
                    matched_name = known_name

            threshold = 1.0  # Adjust based on your model and data
            if min_distance < threshold:
                return matched_name
            else:
                return 'Unknown'
        else:
            return 'No Face Detected'
        
    def async_face_recognition(self, obj_id, img, x1, y1, width, height):
        person_name = self.perform_face_recognition(img, x1, y1, width, height)
        if person_name:
            self.db.update_person_name(obj_id, person_name)
            self.face_recognition_cache[obj_id] = (person_name, self.frame_count)`. Refractor the track_objects function