#!/usr/bin/env python3
"""
Standalone Multi-Face Recognition Application with Adjustable Liveness and Attendance Logging
Features: Real-time detection, adjustable liveness checks (motion, distance), recognition of multiple faces, and local attendance logging.
"""

import cv2
import numpy as np
import customtkinter as ctk
from tkinter import ttk, messagebox
import os
import json
import threading
from PIL import Image, ImageTk
import time
import insightface
import pickle
from collections import deque
import csv
from datetime import datetime

# --- Liveness Detection Helper Functions ---
def check_micro_motion(images):
    """Calculates the average motion between frames."""
    try:
        if len(images) < 2: return 0
        prev_gray = cv2.cvtColor(images[0], cv2.COLOR_BGR2GRAY)
        total_flow = 0
        for i in range(1, len(images)):
            current_gray = cv2.cvtColor(images[i], cv2.COLOR_BGR2GRAY)
            flow = cv2.calcOpticalFlowFarneback(prev_gray, current_gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)
            magnitude, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
            total_flow += np.mean(magnitude)
            prev_gray = current_gray
        return total_flow / (len(images) - 1)
    except Exception: return 0

# Set appearance mode
ctk.set_appearance_mode("dark")

class MultiFaceRecognitionApp:
    def __init__(self):
        self.root = ctk.CTk()
        self.root.title("Standalone Multi-Face Recognition w/ Liveness & Attendance")
        self.root.geometry("1400x900")
        
        # Core variables
        self.is_running = False
        self.cap = None
        self.current_camera_index = 0
        self.frame_buffer = deque(maxlen=3)
        
        # InsightFace model
        self.face_app = None
        self.known_face_embeddings = []
        self.known_face_info = []
        
        # Attendance
        self.log_file = "attendance_log.csv"
        self.last_log_time = {}
        self.log_cooldown = 10
        
        # --- Adjustable Settings ---
        self.confidence_threshold = ctk.DoubleVar(value=0.5)
        self.min_motion_threshold = ctk.DoubleVar(value=0.05)
        self.max_motion_threshold = ctk.DoubleVar(value=2.0)
        self.min_face_area = ctk.IntVar(value=20000)
        self.max_face_area = ctk.IntVar(value=150000)
        
        # Load data
        self.load_models()
        self.ensure_log_file_exists()
        
        # Create UI
        self.create_ui()
        
        # Start camera
        self.start_camera()
        
    def load_models(self):
        try:
            self.face_app = insightface.app.FaceAnalysis(providers=['CPUExecutionProvider'])
            self.face_app.prepare(ctx_id=0, det_size=(640, 640))
            print("✅ InsightFace model loaded.")
            self.load_all_face_embeddings()
        except Exception as e:
            messagebox.showerror("Error", f"Failed to load models: {e}")
            self.root.quit()

    def load_all_face_embeddings(self):
        self.known_face_embeddings = []
        self.known_face_info = []
        user_data = {}
        if os.path.exists("user_data.json"):
            with open("user_data.json", "r") as f: user_data = json.load(f)
        embedding_files = [f for f in os.listdir('.') if f.startswith('insightface_embeddings_') and f.endswith('.pkl')]
        if not embedding_files: messagebox.showwarning("Warning", "No 'insightface_embeddings_*.pkl' files found.")
        for file_path in embedding_files:
            try:
                with open(file_path, 'rb') as f:
                    data = pickle.load(f)
                    user_id, user_name = data.get('user_id'), user_data.get(str(data.get('user_id')), {}).get('name', f"User {data.get('user_id')}")
                    for embedding in data.get('embeddings', []):
                        self.known_face_embeddings.append(embedding)
                        self.known_face_info.append((user_name, user_id))
            except Exception as e: print(f"❌ Error loading {file_path}: {e}")
        if self.known_face_embeddings: print(f"✅ Loaded {len(self.known_face_embeddings)} face embeddings.")

    def ensure_log_file_exists(self):
        if not os.path.exists(self.log_file):
            with open(self.log_file, mode='w', newline='') as f:
                csv.writer(f).writerow(["Timestamp", "User ID", "User Name", "Status"])
            print(f"✅ Created log file: {self.log_file}")

    def create_ui(self):
        main_frame = ctk.CTkFrame(self.root)
        main_frame.pack(fill="both", expand=True, padx=10, pady=10)
        title_label = ctk.CTkLabel(main_frame, text="Multi-Face Recognition with Liveness & Attendance", font=ctk.CTkFont(size=24, weight="bold"))
        title_label.pack(pady=(0, 20))
        content_frame = ctk.CTkFrame(main_frame)
        content_frame.pack(fill="both", expand=True)
        left_frame = ctk.CTkFrame(content_frame)
        left_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
        self.camera_label = ctk.CTkLabel(left_frame, text="Camera Loading...", width=640, height=480)
        self.camera_label.pack(pady=10, padx=10)
        controls_frame = ctk.CTkFrame(left_frame)
        controls_frame.pack(fill="x", pady=10, padx=10)
        self.start_button = ctk.CTkButton(controls_frame, text="Start", command=self.start_recognition)
        self.start_button.pack(side="left", expand=True, padx=5, pady=5)
        self.stop_button = ctk.CTkButton(controls_frame, text="Stop", command=self.stop_recognition, state="disabled")
        self.stop_button.pack(side="left", expand=True, padx=5, pady=5)
        self.settings_button = ctk.CTkButton(controls_frame, text="Settings", command=self.open_settings_window)
        self.settings_button.pack(side="right", expand=True, padx=5, pady=5)
        self.status_label = ctk.CTkLabel(left_frame, text="Status: Ready", font=ctk.CTkFont(size=14))
        self.status_label.pack(pady=10)
        right_frame = ctk.CTkFrame(content_frame)
        right_frame.pack(side="right", fill="both", expand=True)
        history_title = ctk.CTkLabel(right_frame, text="Live Attendance Log", font=ctk.CTkFont(size=18, weight="bold"))
        history_title.pack(pady=10)
        columns = ("Time", "User", "Status")
        self.history_tree = ttk.Treeview(right_frame, columns=columns, show="headings", height=20)
        for col in columns: self.history_tree.heading(col, text=col)
        self.history_tree.column("Time", width=100); self.history_tree.column("User", width=150); self.history_tree.column("Status", width=100)
        self.history_tree.pack(fill="both", expand=True, padx=10, pady=10)

    def open_settings_window(self):
        win = ctk.CTkToplevel(self.root)
        win.title("Settings"); win.geometry("500x500"); win.transient(self.root); win.grab_set()
        ctk.CTkLabel(win, text="Adjust Liveness Thresholds", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=20)
        
        def add_slider(parent, label, var, from_, to, steps=100):
            frame = ctk.CTkFrame(parent)
            frame.pack(fill="x", padx=20, pady=10)
            label_widget = ctk.CTkLabel(frame, text=f"{label}: {var.get():.2f}", font=ctk.CTkFont(size=14))
            label_widget.pack()
            slider = ctk.CTkSlider(frame, from_=from_, to=to, number_of_steps=steps, variable=var, command=lambda v, l=label_widget, lbl=label: l.configure(text=f"{lbl}: {v:.2f}"))
            slider.pack(fill="x", padx=10, pady=5)

        add_slider(win, "Min Motion (Higher=More Shake)", self.min_motion_threshold, 0.01, 1.0)
        add_slider(win, "Max Motion (Higher=More Shake)", self.max_motion_threshold, 1.0, 5.0)
        add_slider(win, "Min Face Size (Higher=Closer)", self.min_face_area, 5000, 50000, steps=90)
        add_slider(win, "Max Face Size (Higher=Closer)", self.max_face_area, 60000, 200000, steps=140)
        add_slider(win, "Recognition Confidence", self.confidence_threshold, 0.3, 0.7, steps=40)

        ctk.CTkButton(win, text="Close", command=win.destroy).pack(pady=20)

    def start_camera(self):
        try:
            self.cap = cv2.VideoCapture(self.current_camera_index)
            if not self.cap.isOpened(): messagebox.showerror("Error", f"Could not open camera {self.current_camera_index}")
            self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640); self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
            print(f"✅ Camera {self.current_camera_index} started.")
        except Exception as e: messagebox.showerror("Error", f"Failed to start camera: {e}")
            
    def start_recognition(self):
        if not self.is_running:
            if not self.known_face_embeddings: messagebox.showerror("Error", "No face embeddings loaded.")
            self.is_running = True
            self.start_button.configure(state="disabled"); self.stop_button.configure(state="normal"); self.settings_button.configure(state="disabled")
            self.status_label.configure(text="Status: Recognition Active")
            self.recognition_thread = threading.Thread(target=self.recognition_loop, daemon=True)
            self.recognition_thread.start()
            
    def stop_recognition(self):
        self.is_running = False
        self.start_button.configure(state="normal"); self.stop_button.configure(state="disabled"); self.settings_button.configure(state="normal")
        self.status_label.configure(text="Status: Stopped")
        
    def recognition_loop(self):
        while self.is_running:
            try:
                ret, frame = self.cap.read()
                if not ret: time.sleep(0.01); continue
                
                self.frame_buffer.append(frame)
                if len(self.frame_buffer) < 3: continue

                middle_frame = self.frame_buffer[1]
                faces = self.face_app.get(middle_frame)
                
                for face in faces:
                    bbox = face.bbox.astype(int)

                    is_live = (self.min_motion_threshold.get() < check_micro_motion(list(self.frame_buffer)) < self.max_motion_threshold.get() and
                               self.min_face_area.get() < ((bbox[2] - bbox[0]) * (bbox[3] - bbox[1])) < self.max_face_area.get())

                    if not is_live:
                        cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 0, 255), 2)
                        cv2.putText(frame, "Liveness Failed", (bbox[0], bbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
                        continue

                    cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 255, 0), 2)
                    embedding = face.normed_embedding
                    sims = np.dot(self.known_face_embeddings, embedding.T)
                    best_match_index = np.argmax(sims)
                    confidence = sims[best_match_index]
                    
                    name, user_id = "Unknown", None
                    if confidence > self.confidence_threshold.get():
                        name, user_id = self.known_face_info[best_match_index]
                        self.log_attendance(user_id, name)

                    display_text = f"{name} ({confidence:.2f})"
                    cv2.putText(frame, display_text, (bbox[0], bbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                frame_pil = Image.fromarray(frame_rgb)
                frame_tk = ImageTk.PhotoImage(image=frame_pil)
                self.camera_label.configure(image=frame_tk, text=""); self.camera_label.image = frame_tk
                
            except Exception as e: print(f"Error in recognition loop: {e}"); time.sleep(0.5)

    def log_attendance(self, user_id, user_name):
        current_time = time.time()
        if user_id in self.last_log_time and (current_time - self.last_log_time[user_id]) < self.log_cooldown: return

        now = datetime.now()
        timestamp, today_str = now.strftime("%Y-%m-%d %H:%M:%S"), now.strftime("%Y-%m-%d")
        
        records_today = []
        try:
            with open(self.log_file, 'r', newline='') as f:
                reader = csv.reader(f); next(reader)
                for row in reader:
                    if row[0].startswith(today_str) and row[1] == str(user_id): records_today.append(row)
        except (IOError, IndexError): pass

        status = "Check-in" if not records_today else "Check-out"
        
        with open(self.log_file, 'a', newline='') as f: csv.writer(f).writerow([timestamp, user_id, user_name, status])
        
        self.last_log_time[user_id] = current_time
        print(f"✅ Logged {status} for {user_name} (ID: {user_id})")

        time_str = now.strftime("%H:%M:%S")
        item = self.history_tree.insert("", 0, values=(time_str, user_name, status))
        tag = "checkin" if status == "Check-in" else "checkout"
        self.history_tree.item(item, tags=(tag,)); self.history_tree.tag_configure("checkin", foreground="green"); self.history_tree.tag_configure("checkout", foreground="red")

    def on_closing(self):
        self.is_running = False
        if self.cap: self.cap.release()
        self.root.destroy()
        
    def run(self):
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.root.mainloop()

if __name__ == "__main__":
    app = MultiFaceRecognitionApp()
    app.run()