#!/opt/homebrew/bin/python3.11
"""
Face Processor - AI-Driven Processor Contract Version 1.0

Compliant with AI-Driven Processor Contract v1.0
Effective Date: 2026-03-27

Features:
1. Standardized command-line interface
2. Redis progress reporting
3. Signal handling (SIGTERM, SIGINT)
4. Health check mode
5. Resource monitoring
6. Contract-compliant JSON output
7. Unified configuration
8. Support for multiple face detection methods (Haar Cascade, DNN)
"""

import sys
import json
import os
import argparse
import signal
import time
import subprocess
import traceback
from datetime import datetime
from typing import Dict, Any, Optional, Tuple
import atexit

# Redis Publisher for progress reporting
try:
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from redis_publisher import RedisPublisher

    REDIS_AVAILABLE = True
except ImportError:
    REDIS_AVAILABLE = False
    print(
        "WARNING: RedisPublisher not available, progress reporting disabled",
        file=sys.stderr,
    )

# Contract version
CONTRACT_VERSION = "1.0"
PROCESSOR_NAME = (
    "/Users/accusys/momentry_core_0.1/scripts/face_processor_contract_v1.py"
)
PROCESSOR_VERSION = "1.0.0"
MODEL_NAME = "opencv"
MODEL_VERSION = "4.8"


class FaceProcessor:
    """Face Detection Processor"""

    def __init__(
        self,
        video_path: str,
        output_path: str,
        uuid: Optional[str] = None,
        check_health: bool = False,
    ):
        self.video_path = video_path
        self.output_path = output_path
        self.uuid = uuid
        self.check_health = check_health

        # Configuration from environment variables with defaults
        self.timeout = int(os.environ.get("MOMENTRY_FACE_TIMEOUT", "3600"))
        self.detection_method = os.environ.get("MOMENTRY_FACE_METHOD", "haar")
        self.confidence = float(os.environ.get("MOMENTRY_FACE_CONFIDENCE", "0.5"))
        self.min_face_size = int(os.environ.get("MOMENTRY_FACE_MIN_SIZE", "30"))
        self.max_face_size = int(os.environ.get("MOMENTRY_FACE_MAX_SIZE", "300"))
        self.scale_factor = float(os.environ.get("MOMENTRY_FACE_SCALE_FACTOR", "1.1"))
        self.min_neighbors = int(os.environ.get("MOMENTRY_FACE_MIN_NEIGHBORS", "3"))
        self.gpu_enabled = (
            os.environ.get("MOMENTRY_FACE_GPU", "false").lower() == "true"
        )

        # Initialize Redis publisher if available
        self.publisher = None
        if REDIS_AVAILABLE and uuid:
            self.publisher = RedisPublisher(uuid)

        # State tracking
        self.start_time = None
        self.is_interrupted = False

        # Set up signal handlers
        signal.signal(signal.SIGTERM, self._signal_handler)
        signal.signal(signal.SIGINT, self._signal_handler)

        # Register cleanup
        atexit.register(self._cleanup)

    def _signal_handler(self, signum, frame):
        """Handle termination signals gracefully"""
        self.is_interrupted = True
        self.publish(
            "warning", f"Received signal {signum}, saving progress and exiting..."
        )
        sys.exit(130 if signum == signal.SIGINT else 143)

    def _cleanup(self):
        """Cleanup resources on exit"""
        pass

    def publish(self, level: str, message: str):
        """Publish message to Redis if available"""
        if self.publisher:
            if level == "info":
                self.publisher.info(PROCESSOR_NAME, message)
            elif level == "warning":
                self.publisher.warning(PROCESSOR_NAME, message)
            elif level == "error":
                self.publisher.error(PROCESSOR_NAME, message)
            elif level == "complete":
                self.publisher.complete(PROCESSOR_NAME, message)
        else:
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            print(
                f"[{timestamp}] [{PROCESSOR_NAME}] [{level.upper()}] {message}",
                file=sys.stderr,
            )

    def validate_input(self) -> Tuple[bool, str]:
        """Validate input video file"""
        if not os.path.exists(self.video_path):
            return False, f"Video file not found: {self.video_path}"

        if not self.video_path.lower().endswith(
            (".mp4", ".avi", ".mov", ".mkv", ".webm")
        ):
            return False, f"Unsupported video format: {self.video_path}"

        # Check if output directory is writable
        output_dir = os.path.dirname(self.output_path)
        if output_dir and not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir, exist_ok=True)
            except Exception as e:
                return False, f"Cannot create output directory: {e}"

        return True, "Input validation passed"

    def check_dependencies(self) -> Dict[str, Any]:
        """Check if all dependencies are available"""
        dependencies = {
            "opencv": {"status": "unknown", "version": None},
            "ffprobe": {"status": "unknown", "version": None},
            "redis": {
                "status": "available" if REDIS_AVAILABLE else "unavailable",
                "version": None,
            },
            "python": {"status": "available", "version": sys.version.split()[0]},
        }

        # Check opencv
        try:
            import cv2

            dependencies["opencv"]["status"] = "available"
            dependencies["opencv"]["version"] = cv2.__version__
        except ImportError:
            dependencies["opencv"]["status"] = "unavailable"

        # Check ffprobe
        try:
            result = subprocess.run(
                ["ffprobe", "-version"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                dependencies["ffprobe"]["status"] = "available"
                dependencies["ffprobe"]["version"] = result.stdout.split("\n")[0]
            else:
                dependencies["ffprobe"]["status"] = "unavailable"
        except (subprocess.SubprocessError, FileNotFoundError):
            dependencies["ffprobe"]["status"] = "unavailable"

        return dependencies

    def perform_health_check(self) -> Dict[str, Any]:
        """Perform comprehensive health check"""
        dependencies = self.check_dependencies()

        # Check if essential dependencies are available
        essential_deps = ["opencv", "ffprobe"]
        all_available = all(
            dependencies.get(dep, {}).get("status") == "available"
            for dep in essential_deps
        )

        return {
            "status": "healthy" if all_available else "unhealthy",
            "dependencies": dependencies,
            "contract_version": CONTRACT_VERSION,
            "processor_name": PROCESSOR_NAME,
            "processor_version": PROCESSOR_VERSION,
            "model_name": MODEL_NAME,
            "model_version": MODEL_VERSION,
            "timestamp": datetime.now().isoformat(),
        }

    def process(self) -> Dict[str, Any]:
        """Main processing method"""
        self.start_time = time.time()
        self.publish(
            "info", f"Starting face detection with method: {self.detection_method}"
        )

        # Validate input
        is_valid, message = self.validate_input()
        if not is_valid:
            return {
                "status": "error",
                "error": message,
                "processor_name": PROCESSOR_NAME,
                "processor_version": PROCESSOR_VERSION,
                "timestamp": datetime.now().isoformat(),
            }

        try:
            import cv2
            import numpy as np

            # Load video
            cap = cv2.VideoCapture(self.video_path)
            if not cap.isOpened():
                return {
                    "status": "error",
                    "error": f"Cannot open video file: {self.video_path}",
                    "processor_name": PROCESSOR_NAME,
                    "processor_version": PROCESSOR_VERSION,
                    "timestamp": datetime.now().isoformat(),
                }

            # Get video properties
            fps = cap.get(cv2.CAP_PROP_FPS)
            total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
            width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            duration = total_frames / fps if fps > 0 else 0

            self.publish(
                "info",
                f"Video: {total_frames} frames, {fps:.2f} FPS, {width}x{height}, {duration:.2f}s",
            )

            # Load face detector based on method
            if self.detection_method == "haar":
                # Load Haar Cascade classifier
                cascade_path = (
                    cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
                )
                if not os.path.exists(cascade_path):
                    cascade_path = (
                        cv2.data.haarcascades + "haarcascade_frontalface_alt.xml"
                    )

                face_cascade = cv2.CascadeClassifier(cascade_path)
                if face_cascade.empty():
                    return {
                        "status": "error",
                        "error": f"Failed to load Haar Cascade classifier from {cascade_path}",
                        "processor_name": PROCESSOR_NAME,
                        "processor_version": PROCESSOR_VERSION,
                        "timestamp": datetime.now().isoformat(),
                    }
            elif self.detection_method == "dnn":
                # Load DNN model
                prototxt_path = os.path.join(
                    os.path.dirname(__file__), "models", "deploy.prototxt"
                )
                model_path = os.path.join(
                    os.path.dirname(__file__),
                    "models",
                    "res10_300x300_ssd_iter_140000.caffemodel",
                )

                if not os.path.exists(prototxt_path) or not os.path.exists(model_path):
                    return {
                        "status": "error",
                        "error": f"DNN model files not found. Please ensure {prototxt_path} and {model_path} exist",
                        "processor_name": PROCESSOR_NAME,
                        "processor_version": PROCESSOR_VERSION,
                        "timestamp": datetime.now().isoformat(),
                    }

                net = cv2.dnn.readNetFromCaffe(prototxt_path, model_path)
                if self.gpu_enabled:
                    net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
                    net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
            else:
                return {
                    "status": "error",
                    "error": f"Unsupported detection method: {self.detection_method}",
                    "processor_name": PROCESSOR_NAME,
                    "processor_version": PROCESSOR_VERSION,
                    "timestamp": datetime.now().isoformat(),
                }

            # Process frames
            frame_count = 0
            faces_detected = 0
            detection_results = []

            while not self.is_interrupted:
                ret, frame = cap.read()
                if not ret:
                    break

                frame_count += 1

                # Report progress every 100 frames
                if frame_count % 100 == 0:
                    progress = (frame_count / total_frames) * 100
                    self.publish(
                        "info",
                        f"Processed {frame_count}/{total_frames} frames ({progress:.1f}%)",
                    )

                # Convert to grayscale for Haar Cascade
                if self.detection_method == "haar":
                    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                    faces = face_cascade.detectMultiScale(
                        gray,
                        scaleFactor=self.scale_factor,
                        minNeighbors=self.min_neighbors,
                        minSize=(self.min_face_size, self.min_face_size),
                        maxSize=(self.max_face_size, self.max_face_size),
                    )

                    for x, y, w, h in faces:
                        faces_detected += 1
                        detection_results.append(
                            {
                                "frame": frame_count,
                                "timestamp": frame_count / fps,
                                "x": int(x),
                                "y": int(y),
                                "width": int(w),
                                "height": int(h),
                                "confidence": 1.0,  # Haar Cascade doesn't provide confidence
                                "method": "haar",
                            }
                        )

                elif self.detection_method == "dnn":
                    # Prepare input blob
                    blob = cv2.dnn.blobFromImage(
                        cv2.resize(frame, (300, 300)),
                        1.0,
                        (300, 300),
                        (104.0, 177.0, 123.0),
                    )
                    net.setInput(blob)
                    detections = net.forward()

                    for i in range(detections.shape[2]):
                        confidence = detections[0, 0, i, 2]
                        if confidence > self.confidence:
                            faces_detected += 1
                            box = detections[0, 0, i, 3:7] * np.array(
                                [width, height, width, height]
                            )
                            (x, y, x2, y2) = box.astype("int")
                            w = x2 - x
                            h = y2 - y

                            detection_results.append(
                                {
                                    "frame": frame_count,
                                    "timestamp": frame_count / fps,
                                    "x": int(x),
                                    "y": int(y),
                                    "width": int(w),
                                    "height": int(h),
                                    "confidence": float(confidence),
                                    "method": "dnn",
                                }
                            )

                # Check timeout
                if time.time() - self.start_time > self.timeout:
                    self.publish(
                        "warning",
                        f"Timeout reached ({self.timeout}s), stopping processing",
                    )
                    break

            cap.release()

            # Save results
            result_data = {
                "status": "success",
                "processor_name": PROCESSOR_NAME,
                "processor_version": PROCESSOR_VERSION,
                "contract_version": CONTRACT_VERSION,
                "model_name": MODEL_NAME,
                "model_version": MODEL_VERSION,
                "timestamp": datetime.now().isoformat(),
                "video_info": {
                    "path": self.video_path,
                    "frames": total_frames,
                    "fps": fps,
                    "width": width,
                    "height": height,
                    "duration": duration,
                },
                "processing_info": {
                    "method": self.detection_method,
                    "confidence_threshold": self.confidence,
                    "min_face_size": self.min_face_size,
                    "max_face_size": self.max_face_size,
                    "scale_factor": self.scale_factor,
                    "min_neighbors": self.min_neighbors,
                    "gpu_enabled": self.gpu_enabled,
                },
                "results": {
                    "frames_processed": frame_count,
                    "faces_detected": faces_detected,
                    "detections": detection_results,
                },
            }

            # Write output
            with open(self.output_path, "w") as f:
                json.dump(result_data, f, indent=2)

            processing_time = time.time() - self.start_time
            self.publish(
                "complete",
                f"Face detection completed: {faces_detected} faces detected in {frame_count} frames ({processing_time:.1f}s)",
            )

            return {
                "status": "success",
                "frames_processed": frame_count,
                "faces_detected": faces_detected,
                "output_file": self.output_path,
                "processing_time": processing_time,
                "processor_name": PROCESSOR_NAME,
                "processor_version": PROCESSOR_VERSION,
                "timestamp": datetime.now().isoformat(),
            }

        except Exception as e:
            error_msg = f"Error during face detection: {str(e)}"
            self.publish("error", error_msg)
            return {
                "status": "error",
                "error": error_msg,
                "traceback": traceback.format_exc(),
                "processor_name": PROCESSOR_NAME,
                "processor_version": PROCESSOR_VERSION,
                "timestamp": datetime.now().isoformat(),
            }


def main():
    """Main entry point"""
    parser = argparse.ArgumentParser(
        description="Face Processor - AI-Driven Processor Contract Version 1.0"
    )
    parser.add_argument("video_path", help="Path to input video file")
    parser.add_argument("output_path", help="Path where JSON output should be written")
    parser.add_argument("--uuid", "-u", help="UUID for Redis progress reporting")
    parser.add_argument(
        "--check-health", action="store_true", help="Perform health check and exit"
    )

    args = parser.parse_args()

    # Create processor instance
    processor = FaceProcessor(
        video_path=args.video_path,
        output_path=args.output_path,
        uuid=args.uuid,
        check_health=args.check_health,
    )

    # Health check mode
    if args.check_health:
        health_result = processor.perform_health_check()
        print(json.dumps(health_result, indent=2))
        sys.exit(0 if health_result["status"] == "healthy" else 1)

    # Process video
    try:
        result = processor.process()

        # Print result summary
        if result["status"] == "success":
            print(f"Successfully processed {result['frames_processed']} frames")
            print(f"Detected {result['faces_detected']} faces")
            print(f"Output saved to: {result['output_file']}")
        else:
            print(f"Error: {result.get('error', 'Unknown error')}")
            sys.exit(1)

    except KeyboardInterrupt:
        print("\nProcessing interrupted by user")
        sys.exit(130)
    except Exception as e:
        print(f"Fatal error: {e}")
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
