from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask import Response
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
from dotenv import load_dotenv
from flask import current_app
import jwt
import cv2
import easyocr
import numpy as np
import threading
import time
from functools import wraps
import re
import os
from sqlalchemy.exc import SQLAlchemyError, OperationalError
from urllib.parse import urlparse
import logging
from apscheduler.schedulers.background import BackgroundScheduler

load_dotenv()

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
log_level = os.getenv('LOG_LEVEL', 'INFO')
logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))

app = Flask(__name__)
CORS(app)

# ==================== KONFIGURASI ====================
# GUNAKAN ENVIRONMENT VARIABLES UNTUK PRODUCTION
# Baris 31-36
DB_USER = os.getenv('DB_USER')
DB_PASS = os.getenv('DB_PASS')
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = os.getenv('DB_PORT', '3306')
DB_NAME = os.getenv('DB_NAME')
SECRET_KEY = os.getenv('SECRET_KEY')

# Validasi environment variables wajib
required_vars = {
    'DB_USER': DB_USER,
    'DB_PASS': DB_PASS,
    'DB_NAME': DB_NAME,
    'SECRET_KEY': SECRET_KEY
}

missing_vars = [key for key, value in required_vars.items() if not value]
if missing_vars:
    logger.error(f"Missing required environment variables: {', '.join(missing_vars)}")
    print("\n" + "="*60)
    print("ERROR: Missing Environment Variables!")
    print("="*60)
    print("\nBuat file .env dengan isi:")
    print("DB_USER=your_db_user")
    print("DB_PASS=your_strong_password")
    print("DB_NAME=anpr_db")
    print("SECRET_KEY=your_secret_key_64_chars")
    print("\nGenerate SECRET_KEY dengan: python -c 'import secrets; print(secrets.token_hex(32))'")
    print("="*60 + "\n")
    exit(1)

app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
    'pool_pre_ping': True,
    'pool_recycle': 3600,
    'pool_size': 10,
    'max_overflow': 20
}
app.config['SECRET_KEY'] = SECRET_KEY

# OPTIMASI: Konfigurasi performa
DETECTION_INTERVAL = 10  # Turunkan dari 15 → 10 frame (lebih sering scan)
FRAME_RESIZE = (1280, 720)  # Naikkan dari (640, 480) untuk OCR lebih akurat
FPS_LIMIT = 0.03  # Turunkan dari 0.05 untuk framerate lebih tinggi
USE_GPU = False  # Aktifkan jika punya GPU NVIDIA
FPS_LIMIT = 0.05  # Sleep 0.05 detik = max ~20 FPS
DB_UPDATE_INTERVAL = 500  # Update database setiap 500 frame, bukan 100
#MAX_CONCURRENT_CAMERAS = int(os.getenv('MAX_CONCURRENT_CAMERAS', '5'))  # TAMBAHKAN INI

db = SQLAlchemy(app)

# ==================== MODEL DATABASE ====================
class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False, index=True)
    password = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(20), nullable=False, default='user')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'role': self.role,
            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
        }

class SystemSetting(db.Model):
    __tablename__ = 'system_settings'
    
    id = db.Column(db.Integer, primary_key=True)
    max_cameras = db.Column(db.Integer, default=0)  # 0 = unlimited
    enable_limit = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'max_cameras': self.max_cameras,
            'enable_limit': self.enable_limit,
            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
        }

class Camera(db.Model):
    __tablename__ = 'cameras'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    rtsp_url = db.Column(db.String(500), nullable=False)
    location = db.Column(db.String(200))
    status = db.Column(db.String(20), default='offline', index=True)
    is_monitoring = db.Column(db.Boolean, default=False)
    last_check = db.Column(db.DateTime)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    records = db.relationship('PlateRecord', backref='camera', lazy='dynamic', cascade='all, delete-orphan')
    
    def to_dict(self):
        return {
            'id': self.id,
            'name': self.name,
            'rtsp_url': self.rtsp_url,
            'location': self.location,
            'status': self.status,
            'is_monitoring': self.is_monitoring,
            'last_check': self.last_check.strftime('%Y-%m-%d %H:%M:%S') if self.last_check else None,
            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
            'total_records': self.records.count()
        }

class PlateRecord(db.Model):
    __tablename__ = 'plate_records'
    
    id = db.Column(db.Integer, primary_key=True)
    camera_id = db.Column(db.Integer, db.ForeignKey('cameras.id'), nullable=False, index=True)
    plate_number = db.Column(db.String(20), nullable=False, index=True)
    confidence = db.Column(db.Float)
    image_path = db.Column(db.String(500))
    vehicle_type = db.Column(db.String(50))
    detected_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
    
    def to_dict(self):
        return {
            'id': self.id,
            'camera_id': self.camera_id,
            'camera_name': self.camera.name,
            'camera_location': self.camera.location,
            'plate_number': self.plate_number,
            'confidence': round(self.confidence * 100, 2) if self.confidence else 0,
            'vehicle_type': self.vehicle_type,
            'detected_at': self.detected_at.strftime('%Y-%m-%d %H:%M:%S'),
            'image_path': self.image_path
        }

class AutoDeleteSetting(db.Model):
    __tablename__ = 'auto_delete_settings'
    
    id = db.Column(db.Integer, primary_key=True)
    is_enabled = db.Column(db.Boolean, default=False)
    delete_after_days = db.Column(db.Integer, default=7)
    last_cleanup = db.Column(db.DateTime)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'is_enabled': self.is_enabled,
            'delete_after_days': self.delete_after_days,
            'last_cleanup': self.last_cleanup.strftime('%Y-%m-%d %H:%M:%S') if self.last_cleanup else None,
            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
        }

# ==================== GLOBAL VARIABLES ====================
try:
    logger.info("Loading EasyOCR model...")
    reader = easyocr.Reader(['en'], gpu=USE_GPU)
    logger.info(f"EasyOCR model loaded successfully! (GPU: {USE_GPU})")
except Exception as e:
    logger.error(f"Failed to load EasyOCR: {str(e)}")
    reader = None

monitoring_threads = {}
active_streams = {}
streams_lock = threading.Lock()  # Tambahkan lock
plate_votes = {}  # {camera_id: {plate_text: [confidence_list]}}
votes_lock = threading.Lock()

# ==================== SCHEDULER FOR AUTO DELETE ====================
def cleanup_old_plates():
    """Hapus data plat nomor yang sudah melewati batas waktu"""
    with app.app_context():
        try:
            setting = db.session.get(AutoDeleteSetting, 1)
            if not setting or not setting.is_enabled:
                return
            
            cutoff_date = datetime.utcnow() - timedelta(days=setting.delete_after_days)
            deleted_count = PlateRecord.query.filter(PlateRecord.detected_at < cutoff_date).delete()
            
            setting.last_cleanup = datetime.utcnow()
            db.session.commit()
            
            if deleted_count > 0:
                logger.info(f"Auto-cleanup: Deleted {deleted_count} old plate records (older than {setting.delete_after_days} days)")
        except Exception as e:
            logger.error(f"Error in auto-cleanup: {str(e)}")
            db.session.rollback()

scheduler = BackgroundScheduler()
scheduler.add_job(func=cleanup_old_plates, trigger="interval", hours=1)
scheduler.start()

# ==================== HELPER FUNCTIONS ====================
def validate_rtsp_url(url):
    """Validasi format RTSP URL"""
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ['rtsp', 'rtmp', 'http', 'https']:
            return False, "URL harus menggunakan protokol rtsp://, rtmp://, http://, atau https://"
        if not parsed.netloc:
            return False, "URL tidak valid, pastikan format: rtsp://username:password@ip:port/stream"
        return True, None
    except Exception as e:
        return False, f"URL tidak valid: {str(e)}"

def handle_db_error(func):
    """Decorator untuk menangani database errors"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except OperationalError as e:
            logger.error(f"Database connection error: {str(e)}")
            db.session.rollback()
            return jsonify({
                'success': False, 
                'message': 'Koneksi database bermasalah. Silakan coba lagi.'
            }), 503
        except SQLAlchemyError as e:
            logger.error(f"Database error: {str(e)}")
            db.session.rollback()
            return jsonify({
                'success': False, 
                'message': 'Terjadi kesalahan database. Silakan coba lagi.'
            }), 500
        except Exception as e:
            logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
            return jsonify({
                'success': False, 
                'message': f'Terjadi kesalahan: {str(e)}'
            }), 500
    return wrapper

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'success': False, 'message': 'Token diperlukan'}), 401
        
        try:
            if token.startswith('Bearer '):
                token = token[7:]
            
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
            current_user = db.session.get(User, data['user_id'])
            
            if not current_user:
                return jsonify({'success': False, 'message': 'User tidak ditemukan'}), 401
                
        except jwt.ExpiredSignatureError:
            return jsonify({'success': False, 'message': 'Token expired, silakan login kembali'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'success': False, 'message': 'Token tidak valid'}), 401
        except Exception as e:
            logger.error(f"Token validation error: {str(e)}")
            return jsonify({'success': False, 'message': 'Autentikasi gagal'}), 401
        
        return f(current_user, *args, **kwargs)
    
    return decorated

def admin_required(f):
    @wraps(f)
    def decorated(current_user, *args, **kwargs):
        if current_user.role != 'admin':
            return jsonify({'success': False, 'message': 'Akses ditolak. Hanya admin yang diizinkan'}), 403
        return f(current_user, *args, **kwargs)
    
    return decorated

def extract_plate_region(image):
    """
    Ekstrak region plat nomor dari frame kamera
    Menggunakan deteksi kontour dan aspect ratio untuk menemukan kandidat plat
    
    Args:
        image: Frame BGR dari OpenCV
        
    Returns:
        roi: Region of Interest yang kemungkinan berisi plat nomor
    """
    h, w = image.shape[:2] if len(image.shape) == 2 else image.shape[:2]
    
    # METHOD 1: Simple ROI (fallback)
    # Plat biasanya di area tengah-bawah frame
    roi_y_start = int(h * 0.5)
    roi_y_end = int(h * 0.85)
    roi_x_start = int(w * 0.1)
    roi_x_end = int(w * 0.9)
    
    simple_roi = image[roi_y_start:roi_y_end, roi_x_start:roi_x_end]
    
    try:
        # METHOD 2: Contour-based detection (advanced)
        # Convert to grayscale
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        
        # Apply bilateral filter to reduce noise while keeping edges
        blur = cv2.bilateralFilter(gray, 11, 17, 17)
        
        # Edge detection
        edges = cv2.Canny(blur, 30, 200)
        
        # Find contours
        contours, _ = cv2.findContours(edges.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        
        # Sort by area (largest first)
        contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10]
        
        plate_candidates = []
        
        for contour in contours:
            # Approximate contour
            perimeter = cv2.arcLength(contour, True)
            approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
            
            # Plat nomor biasanya rectangular (4 corners)
            if len(approx) == 4:
                x, y, w_box, h_box = cv2.boundingRect(approx)
                
                # Aspect ratio plat Indonesia: 2:1 sampai 5:1
                aspect_ratio = w_box / float(h_box)
                
                # Area minimum (filter noise)
                area = cv2.contourArea(contour)
                min_area = (w * h) * 0.01  # Min 1% dari total frame
                
                if 2.0 <= aspect_ratio <= 5.0 and area > min_area:
                    plate_candidates.append({
                        'box': (x, y, w_box, h_box),
                        'area': area,
                        'ratio': aspect_ratio
                    })
        
        # Jika ada kandidat plat yang terdeteksi
        if plate_candidates:
            # Pilih kandidat terbaik (area terbesar)
            best = max(plate_candidates, key=lambda x: x['area'])
            x, y, w_box, h_box = best['box']
            
            # Expand ROI sedikit untuk margin
            margin_x = int(w_box * 0.1)
            margin_y = int(h_box * 0.1)
            
            x1 = max(0, x - margin_x)
            y1 = max(0, y - margin_y)
            x2 = min(w, x + w_box + margin_x)
            y2 = min(h, y + h_box + margin_y)
            
            advanced_roi = image[y1:y2, x1:x2]
            
            # Validasi ukuran ROI
            if advanced_roi.shape[0] > 20 and advanced_roi.shape[1] > 50:
                logger.debug(f"Using advanced ROI detection (aspect={best['ratio']:.2f})")
                return advanced_roi
        
        # Fallback ke simple ROI
        logger.debug("Using simple ROI (fallback)")
        return simple_roi
        
    except Exception as e:
        logger.error(f"Error in extract_plate_region: {str(e)}")
        # Fallback ke simple ROI
        return simple_roi

def is_valid_indonesian_plate(text):
    """
    Validasi ketat format plat Indonesia
    Format: [1-2 HURUF][1-4 ANGKA][1-3 HURUF]
    Contoh: B1234CD, A6833WAL, L789AB
    """
    if len(text) < 5 or len(text) > 9:
        return False
    
    # Cari section boundaries
    first_digit_idx = None
    last_digit_idx = None
    
    for i, ch in enumerate(text):
        if ch.isdigit():
            if first_digit_idx is None:
                first_digit_idx = i
            last_digit_idx = i
    
    # Harus ada angka
    if first_digit_idx is None:
        return False
    
    # Section 1: Kode daerah (1-2 huruf)
    prefix = text[:first_digit_idx]
    if len(prefix) < 1 or len(prefix) > 2:
        return False
    if not prefix.isalpha():
        return False
    
    # Section 2: Nomor (1-4 angka)
    number = text[first_digit_idx:last_digit_idx+1]
    if len(number) < 1 or len(number) > 4:
        return False
    if not number.isdigit():
        return False
    
    # Section 3: Seri (1-3 huruf) - RELAKSASI: boleh kosong untuk fallback
    suffix = text[last_digit_idx+1:]
    if len(suffix) > 3:  # UBAH: hapus validasi minimum
        return False
    if suffix and not suffix.isalpha():  # UBAH: hanya validasi jika ada suffix
        return False
    
    # Jika tidak ada suffix, log warning tapi tetap terima
    if len(suffix) == 0:
        logger.warning(f"Plate without suffix accepted (partial): {text}")
    
    # Validasi kode daerah Indonesia (opsional, tapi lebih akurat)
    valid_prefixes = [
        'A', 'B', 'D', 'E', 'F', 'G', 'H', 'K', 'L', 'M', 'N', 'P', 
        'R', 'S', 'T', 'W', 'Z',  # Kode 1 huruf
        'AA', 'AB', 'AD', 'AE', 'AG', 'BA', 'BB', 'BD', 'BE', 'BG',
        'BH', 'BK', 'BL', 'BM', 'BN', 'BP', 'BR', 'BT', 'CC', 'CD',
        'DA', 'DB', 'DC', 'DD', 'DE', 'DG', 'DH', 'DK', 'DL', 'DM',
        'DN', 'DP', 'DR', 'DS', 'DT', 'EA', 'EB', 'ED', 'KA', 'KB',
        'KH', 'KT', 'PA', 'PB'  # Kode 2 huruf populer
    ]
    
    if prefix not in valid_prefixes:
        logger.debug(f"Unknown area code: {prefix} (might be valid, accepting anyway)")
    
    return True

def detect_night_vision_mode(gray_image):
    """
    Deteksi apakah kamera dalam mode night vision/IR
    Ciri: Kontras rendah, warna monochrome, noise pattern khusus
    
    Returns:
        bool: True jika night vision, False jika normal
    """
    # Hitung statistik gambar
    mean_val = np.mean(gray_image)
    std_val = np.std(gray_image)
    
    # Night vision biasanya punya:
    # 1. Mean value ekstrem (sangat gelap atau terang)
    # 2. Std deviation rendah (kontras rendah)
    # 3. Histogram terpusat
    
    histogram = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
    hist_peak = np.argmax(histogram)
    
    # Kriteria night vision
    is_low_contrast = std_val < 40  # Kontras rendah
    is_extreme_brightness = mean_val < 60 or mean_val > 195  # Terlalu gelap/terang
    is_peaked_histogram = histogram[hist_peak] > (gray_image.size * 0.3)  # Histogram terpusat
    
    # Jika 2 dari 3 kriteria terpenuhi, anggap night vision
    score = sum([is_low_contrast, is_extreme_brightness, is_peaked_histogram])
    
    return score >= 2

def adjust_gamma(image, gamma=1.0):
    """
    Gamma correction untuk mencerahkan/menggelapkan gambar
    gamma < 1: Brighten
    gamma > 1: Darken
    gamma = 1: No change
    """
    invGamma = 1.0 / gamma
    table = np.array([((i / 255.0) ** invGamma) * 255
                      for i in np.arange(0, 256)]).astype("uint8")
    return cv2.LUT(image, table)

def contrast_stretching(image):
    """
    Contrast stretching untuk meningkatkan kontras gambar
    Berguna untuk night vision dengan kontras rendah
    """
    # Cari min dan max pixel value
    min_val = np.min(image)
    max_val = np.max(image)
    
    # Avoid division by zero
    if max_val == min_val:
        return image
    
    # Stretch contrast ke full range [0, 255]
    stretched = ((image - min_val) / (max_val - min_val) * 255).astype(np.uint8)
    return stretched

def unsharp_mask(image, kernel_size=(5, 5), sigma=1.0, amount=1.0):
    """
    Unsharp masking untuk sharpening gambar
    Berguna untuk gambar blur dari kamera
    """
    blurred = cv2.GaussianBlur(image, kernel_size, sigma)
    sharpened = float(amount + 1) * image - float(amount) * blurred
    sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
    return sharpened

def format_plate_display(plate):
    """
    Format plat untuk display dengan spasi
    Input: B1234CD
    Output: B 1234 CD
    """
    if len(plate) < 5:
        return plate
    
    # Cari boundaries
    first_digit_idx = None
    last_digit_idx = None
    
    for i, ch in enumerate(plate):
        if ch.isdigit():
            if first_digit_idx is None:
                first_digit_idx = i
            last_digit_idx = i
    
    if first_digit_idx is None:
        return plate
    
    prefix = plate[:first_digit_idx]
    number = plate[first_digit_idx:last_digit_idx+1]
    suffix = plate[last_digit_idx+1:]
    
    return f"{prefix} {number} {suffix}"
        
def detect_indonesian_plate(image, camera_id=None):
    """
    Deteksi plat nomor Indonesia dengan AKURASI TINGGI
    Format: [KODE 1-2 HURUF] [NOMOR 1-4 ANGKA] [SERI 1-3 HURUF]
    
    Returns:
        tuple: (plate_text, confidence) atau (None, 0) jika tidak terdeteksi
    """
    if reader is None:
        logger.error("EasyOCR not initialized")
        return None, 0

    try:
        # 1. RESIZE OPTIMAL
        frame_resized = cv2.resize(image, (1280, 720))
        h, w = frame_resized.shape[:2]
        
        # 2. DETEKSI MODE KAMERA (Night Vision vs Normal)
        gray_full = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2GRAY)
        is_night_vision = detect_night_vision_mode(gray_full)
        
        if is_night_vision:
            logger.debug(f"[Cam{camera_id}] Night vision mode detected")
        
        # 3. ROI LEBIH KETAT - fokus area plat
        roi_y_start = int(h * 0.50)
        roi_y_end = int(h * 0.80)
        roi_x_start = int(w * 0.20)
        roi_x_end = int(w * 0.80)
        roi = frame_resized[roi_y_start:roi_y_end, roi_x_start:roi_x_end]
        
        # 4. PREPROCESSING ADAPTIF BERDASARKAN MODE
        gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
        
        variants = []

        if is_night_vision:
            # === NIGHT VISION / IR CAMERA PREPROCESSING ===
            
            # Method 1: Gamma Correction untuk IR (brightening)
            gamma_corrected = adjust_gamma(gray, gamma=1.5)
            variants.append(("IR_Gamma", gamma_corrected))
            
            # Method 2: CLAHE Aggressive untuk IR
            clahe_ir = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(4,4))
            enhanced_ir = clahe_ir.apply(gray)
            variants.append(("IR_CLAHE", enhanced_ir))
            
            # Method 3: Bilateral + Adaptive untuk IR noise
            bilateral_ir = cv2.bilateralFilter(gray, 7, 50, 50)
            adaptive_ir = cv2.adaptiveThreshold(
                bilateral_ir, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
                cv2.THRESH_BINARY, 15, 3
            )
            variants.append(("IR_Adaptive", adaptive_ir))
            
            # Method 4: Morphological Enhancement untuk IR
            kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
            morph_ir = cv2.morphologyEx(enhanced_ir, cv2.MORPH_CLOSE, kernel)
            _, thresh_ir = cv2.threshold(morph_ir, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
            variants.append(("IR_Morph", thresh_ir))
            
            # Method 5: Contrast Stretching untuk IR
            stretched = contrast_stretching(gray)
            variants.append(("IR_Stretch", stretched))
            
        else:
            # === NORMAL COLOR CAMERA PREPROCESSING ===
            
            # Method 1: CLAHE Standard
            clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
            enhanced = clahe.apply(gray)
            variants.append(("CLAHE", enhanced))

            # Method 2: Bilateral + Otsu
            bilateral = cv2.bilateralFilter(gray, 9, 75, 75)
            _, otsu = cv2.threshold(bilateral, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
            variants.append(("Otsu", otsu))

            # Method 3: Adaptive Gaussian
            adaptive = cv2.adaptiveThreshold(
                gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
                cv2.THRESH_BINARY, 11, 2
            )
            variants.append(("Adaptive", adaptive))
            
            # Method 4: Unsharp Masking (untuk sharpening)
            unsharp = unsharp_mask(gray)
            variants.append(("Unsharp", unsharp))
        
        # 5. RUN OCR PADA SEMUA VARIANT
        all_results = []
        
        for variant_name, variant_img in variants:
            try:
                results = reader.readtext(
                    variant_img,
                    paragraph=False,
                    batch_size=1,
                    decoder='beamsearch',
                    beamWidth=15,
                    text_threshold=0.2,
                    low_text=0.05,
                    link_threshold=0.2,
                    canvas_size=3840,
                    mag_ratio=2.0,
                    allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
                )
                
                for bbox, text, conf in results:
                    all_results.append((bbox, text, conf, variant_name))
                    logger.info(f"[Cam{camera_id}] [{variant_name}] RAW OCR: '{text}' (conf={conf:.3f})")
                    
            except Exception as e:
                logger.error(f"[Cam{camera_id}] {variant_name} error: {str(e)}")
        
        logger.info(f"[Cam{camera_id}] Total OCR results: {len(all_results)}")
        
        # 5. PROCESS & VALIDASI
        pattern = r'^[A-Z]{1,2}\d{1,4}[A-Z]{0,3}$'
        candidates = []
        
        for bbox, raw_text, conf, method in all_results:
            # Bersihkan text
            text = re.sub(r'[^A-Za-z0-9]', '', raw_text).upper()
            
            # VALIDASI: Skip jika terlalu pendek atau terlalu panjang
            if len(text) < 5 or len(text) > 9:
                logger.debug(f"[Cam{camera_id}] Skipped '{text}' - invalid length ({len(text)})")
                continue
            
            # KOREKSI CERDAS berbasis posisi
            corrected = smart_correct_plate(text)
            
            logger.debug(f"[Cam{camera_id}] [{method}] '{raw_text}' -> '{text}' -> '{corrected}' (conf={conf:.3f})")
            
            # Validasi pattern
            if re.match(pattern, corrected) and is_valid_indonesian_plate(corrected):
                candidates.append({
                    'plate': corrected,
                    'conf': conf,
                    'method': method,
                    'original': raw_text
                })
            else:
                logger.debug(f"[Cam{camera_id}] Rejected '{corrected}' - failed validation")
        
        # 6. KOMBINASI FRAGMENTS (jika perlu)
        if len(candidates) == 0 and len(all_results) >= 2:
            logger.debug(f"[Cam{camera_id}] Trying fragment combination...")
            candidates.extend(combine_fragments(all_results, camera_id))
        
        # 6.5. JIKA MASIH KOSONG, COBA KOMBINASI SIMPLE (2 FRAGMENTS TERDEKAT)
        if len(candidates) == 0 and len(all_results) >= 2:
            logger.info(f"[Cam{camera_id}] Trying simple 2-fragment combination...")
            candidates.extend(simple_combine_two_fragments(all_results, camera_id))
        
        # 7. PILIH KANDIDAT TERBAIK
        if candidates:
            # Sort by confidence
            candidates.sort(key=lambda x: x['conf'], reverse=True)
            
            best = candidates[0]
            formatted = format_plate_display(best['plate'])
            
            logger.info(f"[Cam{camera_id}] DETECTED: {formatted} (conf={best['conf']:.2f}, method={best['method']}, from='{best['original']}')")
            
            if len(candidates) > 1:
                other_candidates = [(c['plate'], f"{c['conf']:.2f}") for c in candidates[1:]]
                logger.debug(f"[Cam{camera_id}] Other candidates: {other_candidates}")
            
            # RETURN: Pastikan plate_text bukan string kosong
            return best['plate'], best['conf']
        else:
            # FALLBACK: Cek apakah ada hasil OCR yang "hampir valid"
            logger.warning(f"[Cam{camera_id}] No valid candidates, checking fallback...")
            
            for bbox, raw_text, conf, method in all_results:
                text = re.sub(r'[^A-Za-z0-9]', '', raw_text).upper()
                
                # Coba koreksi langsung tanpa validasi ketat
                if 5 <= len(text) <= 9 and conf > 0.40:  # Confidence tinggi
                    corrected = smart_correct_plate(text)
                    
                    # Hitung score validitas (tidak harus 100% valid)
                    score = 0
                    if re.search(r'[A-Z]{1,2}', corrected[:2]): score += 1
                    if re.search(r'\d{1,4}', corrected): score += 1
                    if re.search(r'[A-Z]{1,3}', corrected[-3:]): score += 1
                    
                    # Jika 2 dari 3 kriteria terpenuhi
                    if score >= 2:
                        logger.warning(f"[Cam{camera_id}] FALLBACK: '{corrected}' (conf={conf:.2f}, score={score}/3)")
                        return corrected, conf * 0.7  # Heavy penalty
            
            logger.debug(f"[Cam{camera_id}] No valid plate from {len(all_results)} OCR results")
            # PENTING: Return None bukan string kosong
            return None, 0

    except Exception as e:
        logger.error(f"[Cam{camera_id}] Exception in detect_indonesian_plate: {str(e)}")
        import traceback
        logger.error(traceback.format_exc())
        # PENTING: Return None pada error
        return None, 0

def smart_correct_plate(text):
    """
    Koreksi karakter berdasarkan POSISI dalam plat
    Format Indonesia: [HURUF 1-2][ANGKA 1-4][HURUF 1-3]
    """
    if len(text) < 5:
        return text
    
    # Deteksi boundaries dengan mencari digit sequence TERPANJANG
    digit_start = None
    digit_end = None
    
    for i, ch in enumerate(text):
        if ch.isdigit():
            if digit_start is None:
                digit_start = i
            digit_end = i
    
    # Jika tidak ada digit, anggap semua huruf
    if digit_start is None:
        return ''.join([fix_to_letter(ch) for ch in text])
    
    # PERBAIKAN PENTING: Batasi panjang number section ke max 4 digit
    # Jika ada lebih dari 4 digit berturut-turut, sisanya adalah suffix yang salah baca
    number_section = text[digit_start:digit_end+1]
    
    if len(number_section) > 4:
        # Ada digit berlebih, pasti suffix yang salah baca
        # Contoh: '27622J' -> number='2762', suffix='2J' (2 harus jadi Z)
        digit_end = digit_start + 3  # Max 4 digit (index 0-3 = 4 chars)
        logger.debug(f"Number section too long ({len(number_section)}), truncating to 4 digits")
    
    # Koreksi per section
    result = []
    
    for i, ch in enumerate(text):
        if i < digit_start:
            # Section 1: Kode daerah (huruf)
            result.append(fix_to_letter(ch))
        elif i <= digit_end:
            # Section 2: Nomor (angka) - max 4 digit
            result.append(fix_to_digit(ch))
        else:
            # Section 3: Seri (huruf) - SEMUA karakter di sini HARUS huruf
            if ch.isdigit():
                # PERBAIKAN: Angka di suffix PASTI salah, konversi ke huruf
                suffix_digit_map = {
                    '0': 'O',
                    '1': 'I', 
                    '2': 'Z',
                    '3': 'J',
                    '4': 'A',
                    '5': 'S',
                    '6': 'G',
                    '7': 'T',
                    '8': 'B',
                    '9': 'Q'
                }
                result.append(suffix_digit_map.get(ch, 'O'))
            else:
                result.append(ch.upper())  # Pastikan uppercase
    
    corrected = ''.join(result)
    logger.debug(f"smart_correct_plate: '{text}' → '{corrected}' (digit_end adjusted to {digit_end})")
    return corrected
  
def fix_to_letter(ch):
    """Paksa jadi huruf"""
    mapping = {
        '0': 'O', '1': 'I', '2': 'Z', '3': 'E', '4': 'A',
        '5': 'S', '6': 'G', '7': 'T', '8': 'B', '9': 'Q'
    }
    if ch.isdigit():
        return mapping.get(ch, ch)
    return ch

def fix_to_digit(ch):
    """Paksa jadi angka"""
    mapping = {
        'O': '0', 'I': '1', 'L': '1', 'Z': '2', 'E': '3', 'A': '4',
        'S': '5', 'G': '6', 'T': '7', 'B': '8', 'Q': '9',
        'D': '0', 'J': '3', 'U': '0'
    }
    if ch.isalpha():
        return mapping.get(ch, ch)
    return ch


def combine_fragments(results, camera_id):
    """Gabungkan fragments yang mungkin bagian dari 1 plat"""
    candidates = []
    pattern = r'^[A-Z]{1,2}\d{1,4}[A-Z]{1,3}$'
    
    # Sort by X position
    sorted_results = sorted(results, key=lambda x: x[0][0][0])
    
    for i in range(len(sorted_results)):
        for j in range(i+2, min(i+4, len(sorted_results)+1)):
            parts = []
            confs = []
            
            for k in range(i, j):
                bbox, text, conf, method = sorted_results[k]
                clean = re.sub(r'[^A-Za-z0-9]', '', text).upper()
                parts.append(clean)
                confs.append(conf)
            
            combined = ''.join(parts)
            corrected = smart_correct_plate(combined)
            avg_conf = sum(confs) / len(confs)
            
            if re.match(pattern, corrected) and is_valid_indonesian_plate(corrected):
                logger.debug(f"[Cam{camera_id}] Combined: {parts} → {corrected}")
                candidates.append({
                    'plate': corrected,
                    'conf': avg_conf * 0.9,  # Penalty sedikit untuk kombinasi
                    'method': 'Combined',
                    'original': '+'.join(parts)
                })
    
    return candidates

def simple_combine_two_fragments(results, camera_id):
    """
    Gabungkan 2 fragments yang terpisah
    Contoh: 'B' + '1234CD' = 'B1234CD'
    """
    candidates = []
    pattern = r'^[A-Z]{1,2}\d{1,4}[A-Z]{1,3}$'
    
    # Sort by X position
    sorted_results = sorted(results, key=lambda x: x[0][0][0])
    
    # PRIORITAS 1: Gabungkan prefix (1-2 huruf) dengan number+suffix
    for i in range(len(sorted_results)):
        bbox1, text1, conf1, method1 = sorted_results[i]
        clean1 = re.sub(r'[^A-Za-z0-9]', '', text1).upper()
        
        # Cek apakah ini prefix candidate (1-2 huruf murni)
        if not (1 <= len(clean1) <= 2 and clean1.isalpha()):
            continue
        
        # Cari fragment berikutnya yang dimulai dengan angka
        for j in range(i + 1, min(i + 3, len(sorted_results))):
            bbox2, text2, conf2, method2 = sorted_results[j]
            clean2 = re.sub(r'[^A-Za-z0-9]', '', text2).upper()
            
            # Harus dimulai dengan angka (number+suffix candidate)
            if not clean2 or not clean2[0].isdigit():
                continue
            
            # Gabungkan
            combined = clean1 + clean2
            
            logger.info(f"[Cam{camera_id}] Combining prefix+number: '{clean1}' + '{clean2}' = '{combined}'")
            
            # Validasi panjang
            if len(combined) < 5 or len(combined) > 9:
                logger.debug(f"[Cam{camera_id}] Rejected: length {len(combined)}")
                continue
            
            # Koreksi
            corrected = smart_correct_plate(combined)
            avg_conf = (conf1 + conf2) / 2
            
            logger.info(f"[Cam{camera_id}] After correction: '{corrected}'")
            
            # Validasi pattern
            if re.match(pattern, corrected) and is_valid_indonesian_plate(corrected):
                logger.info(f"[Cam{camera_id}] Valid plate combined: '{clean1}' + '{clean2}' = '{corrected}' (conf={avg_conf:.2f})")
                candidates.append({
                    'plate': corrected,
                    'conf': avg_conf * 0.90,
                    'method': 'PrefixCombine',
                    'original': f'{text1}+{text2}'
                })
                break
    
    # PRIORITAS 2: Kombinasi fragment bersebelahan
    if len(candidates) == 0:
        for i in range(len(sorted_results) - 1):
            bbox1, text1, conf1, method1 = sorted_results[i]
            bbox2, text2, conf2, method2 = sorted_results[i + 1]
            
            clean1 = re.sub(r'[^A-Za-z0-9]', '', text1).upper()
            clean2 = re.sub(r'[^A-Za-z0-9]', '', text2).upper()
            
            if not clean1 or not clean2:
                continue
            
            combined = clean1 + clean2
            
            if len(combined) < 5 or len(combined) > 9:
                continue
            
            corrected = smart_correct_plate(combined)
            avg_conf = (conf1 + conf2) / 2
            
            if re.match(pattern, corrected) and is_valid_indonesian_plate(corrected):
                logger.info(f"[Cam{camera_id}] Adjacent fragments combined: '{clean1}' + '{clean2}' = '{corrected}' (conf={avg_conf:.2f})")
                candidates.append({
                    'plate': corrected,
                    'conf': avg_conf * 0.85,
                    'method': 'AdjacentCombine',
                    'original': f'{text1}+{text2}'
                })
    
    return candidates

def get_best_plate_candidate(camera_id, min_votes=3, vote_window=10):
    """
    Ambil kandidat plat terbaik dari voting system
    
    Args:
        camera_id: ID kamera
        min_votes: Minimum jumlah vote untuk dianggap valid
        vote_window: Maksimal panjang history vote
    
    Returns:
        (plate_text, avg_confidence) atau (None, 0)
    """
    with votes_lock:
        if camera_id not in plate_votes or not plate_votes[camera_id]:
            return None, 0
        
        # Cari plate dengan vote terbanyak
        best_plate = None
        best_count = 0
        best_avg_conf = 0
        
        for plate, confs in plate_votes[camera_id].items():
            count = len(confs)
            avg_conf = sum(confs) / count if count > 0 else 0
            
            # Kandidat harus punya minimal min_votes dan confidence > 0.20  # TURUNKAN dari 0.25
            if count >= min_votes and avg_conf >= 0.20:
                if count > best_count or (count == best_count and avg_conf > best_avg_conf):
                    best_plate = plate
                    best_count = count
                    best_avg_conf = avg_conf
        
        # Clear votes setelah dapat winner
        if best_plate:
            plate_votes[camera_id].clear()
            logger.info(f"🎯 [Cam{camera_id}] Winner: {best_plate} ({best_count} votes, avg_conf={best_avg_conf:.2f})")
        
        return best_plate, best_avg_conf

def normalize_plate(plate_text):
    """
    Normalisasi plat untuk voting - hapus spasi dan karakter non-alfanumerik
    Sehingga 'A 2762 ZJ', 'A2762ZJ', 'A-2762-ZJ' dianggap sama
    """
    return re.sub(r'[^A-Z0-9]', '', plate_text.upper())

def add_plate_vote(camera_id, plate_text, confidence, vote_window=10):
    """Tambahkan vote untuk plate detection"""
    # Normalisasi plate untuk voting
    normalized_plate = normalize_plate(plate_text)
    
    with votes_lock:
        if camera_id not in plate_votes:
            plate_votes[camera_id] = {}
        
        if normalized_plate not in plate_votes[camera_id]:
            plate_votes[camera_id][normalized_plate] = []
        
        # Tambah vote
        plate_votes[camera_id][normalized_plate].append(confidence)
        
        # Trim old votes (keep last vote_window entries)
        if len(plate_votes[camera_id][normalized_plate]) > vote_window:
            plate_votes[camera_id][normalized_plate] = plate_votes[camera_id][normalized_plate][-vote_window:]
        
        vote_count = len(plate_votes[camera_id][normalized_plate])
        logger.debug(f"[Cam{camera_id}] Vote added: {normalized_plate} (conf={confidence:.2f}, total_votes={vote_count})")

def monitor_camera_thread(camera_id):
    """Thread untuk memantau kamera dan mendeteksi plat secara otomatis"""
    
    # Inisialisasi batch variables
    pending_plates = []
    last_commit_time = time.time()
    BATCH_SIZE = 5
    BATCH_TIMEOUT = 30  # detik
    
    with app.app_context():
        camera = Camera.query.get(camera_id)
        if not camera:
            logger.error(f"[Camera {camera_id}] Not found in database")
            return

        camera_name = camera.name
        rtsp_url = camera.rtsp_url
        
        camera.status = 'connecting' 
        camera.is_monitoring = True
        db.session.commit()
        
    logger.info(f"Starting monitoring thread for camera ID: {camera_id} ({camera_name})") 

    # Inisialisasi video capture
    cap = None
    for attempt in range(5):
        try:
            cap = cv2.VideoCapture(rtsp_url)
            
            # Set buffer size kecil untuk mengurangi lag
            cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
            
            if cap.isOpened():
                logger.info(f"Successfully connected to camera {camera_id} ({camera_name})")
                break
            else:
                logger.warning(f"Attempt {attempt+1}/5: Cannot open stream for camera {camera_id}")
        except Exception as e:
            logger.error(f"Error connecting to camera {camera_id}: {str(e)}")
        time.sleep(2)

    if not cap or not cap.isOpened():
        logger.error(f"[Camera {camera_id}] Failed to connect after multiple attempts")
        with app.app_context():
            camera = db.session.get(Camera, camera_id) 
            if camera and camera.is_monitoring:
                camera.status = 'offline'
                camera.is_monitoring = False
                db.session.commit()
        return

    with app.app_context():
        camera = db.session.get(Camera, camera_id)
        if camera:
            camera.status = 'online'
            camera.last_check = datetime.utcnow()
            db.session.commit()

    logger.info(f"Camera {camera_id} ({camera_name}) is now ONLINE")

    frame_count = 0
    consecutive_failures = 0
    max_consecutive_failures = 100

    # ============================================
    # MAIN LOOP
    # ============================================
    while True:
        with streams_lock:
            if camera_id not in active_streams:
                logger.info(f"[Camera {camera_id}] Monitoring stopped externally, exiting loop")
                break

        ret, frame = cap.read()
        
        if not ret or frame is None:
            consecutive_failures += 1
            
            # Log setiap 10 kali gagal
            if consecutive_failures % 10 == 0:
                logger.warning(f"[Camera {camera_id}] Frame read failed {consecutive_failures} times")
            
            # Jika terlalu banyak gagal berturut-turut, reconnect
            if consecutive_failures >= max_consecutive_failures:
                logger.error(f"[Camera {camera_id}] Too many consecutive failures, reconnecting...")
                
                # Tutup connection lama
                if cap:
                    cap.release()
                
                # Coba reconnect
                time.sleep(2)
                cap = cv2.VideoCapture(rtsp_url)
                cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
                
                if cap.isOpened():
                    logger.info(f"[Camera {camera_id}] Reconnected successfully")
                    consecutive_failures = 0
                    
                    with app.app_context():
                        camera = db.session.get(Camera, camera_id)
                        if camera:
                            camera.status = 'online'
                            camera.last_check = datetime.utcnow()
                            db.session.commit()
                else:
                    logger.error(f"[Camera {camera_id}] Reconnection failed, stopping monitoring")
                    
                    with streams_lock:
                        if camera_id in active_streams:
                            del active_streams[camera_id]
                    
                    with app.app_context():
                        camera = db.session.get(Camera, camera_id)
                        if camera:
                            camera.status = 'offline'
                            camera.is_monitoring = False
                            db.session.commit()
                    break
            
            # Skip ke frame berikutnya
            time.sleep(0.01)
            continue

        # Reset counter jika berhasil baca frame
        consecutive_failures = 0
        frame_count += 1

        # Proses OCR setiap DETECTION_INTERVAL frame
        if frame_count % DETECTION_INTERVAL == 0:
            logger.info(f"[Camera {camera_id}] Running OCR on frame {frame_count}")
            plate_text, conf = detect_indonesian_plate(frame, camera_id)

            # VOTING SYSTEM: Kumpulkan votes dulu, jangan langsung simpan
            if plate_text and len(plate_text) >= 5 and conf > 0.20:
                '''
                # PERBAIKAN: Jika plat tidak lengkap (tanpa suffix), tandai untuk penggabungan
                has_suffix = bool(re.search(r'[A-Z]{1,3}$', plate_text))
                
                if not has_suffix:
                    logger.warning(f"[Camera {camera_id}] Incomplete plate detected: {plate_text} (missing suffix)")
                    # Simpan temporary untuk digabung nanti
                    # Skip voting untuk plat tidak lengkap
                    continue
                '''
                
                add_plate_vote(camera_id, plate_text, conf)
                
                # Cek apakah sudah ada winner
                winner_plate, winner_conf = get_best_plate_candidate(camera_id, min_votes=2)
                
                if winner_plate:
                    with app.app_context():
                        try:
                            # Cek duplikasi dalam 10 detik terakhir
                            recent = PlateRecord.query.filter_by(plate_number=winner_plate)\
                                .order_by(PlateRecord.detected_at.desc()).first()
                            
                            if not recent or (datetime.utcnow() - recent.detected_at).total_seconds() > 10:
                                new_plate = PlateRecord(
                                    plate_number=winner_plate,
                                    confidence=winner_conf,
                                    camera_id=camera_id,
                                    detected_at=datetime.utcnow()
                                )
                                pending_plates.append(new_plate)
                                
                                current_time = time.time()
                                
                                # Commit jika batch penuh ATAU timeout
                                if len(pending_plates) >= BATCH_SIZE or \
                                   (current_time - last_commit_time) >= BATCH_TIMEOUT:
                                    
                                    db.session.bulk_save_objects(pending_plates)
                                    db.session.commit()
                                    
                                    logger.info(f"Saved {len(pending_plates)} plates to DB (Camera {camera_id})")
                                    
                                    pending_plates.clear()
                                    last_commit_time = current_time

                                logger.info(f"Plate {winner_plate} CONFIRMED and queued (Camera {camera_id}, conf={winner_conf:.2f})")

                            else:
                                logger.info(f"Skipped duplicate plate {winner_plate} (recent detection)")

                        except Exception as e:
                            db.session.rollback()
                            logger.error(f"Failed to save plate {winner_plate}: {str(e)}")
                else:
                    logger.debug(f"[Camera {camera_id}] Collecting votes... ({plate_text})")
            else:
                # Log jika tidak ada plat terdeteksi
                if plate_text:
                    logger.debug(f"[Camera {camera_id}] Low confidence: '{plate_text}' (conf={conf:.2f})")
                else:
                    logger.debug(f"[Camera {camera_id}] No valid plate detected on frame {frame_count}")

        time.sleep(FPS_LIMIT) 

    # ============================================
    # CLEANUP - Dipanggil saat loop selesai
    # ============================================
    
    # 1. Release camera
    if cap:
        cap.release()
        logger.info(f"[Camera {camera_id}] Camera released")
    
    # 2. Commit remaining plates
    if pending_plates:
        with app.app_context():
            try:
                db.session.bulk_save_objects(pending_plates)
                db.session.commit()
                logger.info(f"Final commit: {len(pending_plates)} plates (Camera {camera_id})")
            except Exception as e:
                logger.error(f"Failed to commit remaining plates: {str(e)}")
                db.session.rollback()
    
    # 3. Clear votes
    with votes_lock:
        if camera_id in plate_votes:
            del plate_votes[camera_id]
            logger.info(f"[Camera {camera_id}] Votes cleared")
    
    # 4. Update database status
    with app.app_context():
        camera = db.session.get(Camera, camera_id)
        if camera:
            with streams_lock:
                if camera_id in monitoring_threads:
                    del monitoring_threads[camera_id]

            if camera.is_monitoring:
                camera.status = 'offline'
                camera.is_monitoring = False
                camera.last_check = datetime.utcnow()
                db.session.commit()
        
    logger.info(f"Monitoring thread for camera {camera_id} finished cleanup.")

# ==================== API ROUTES ====================

@app.route('/api/auth/login', methods=['POST'])
@handle_db_error
def login():
    data = request.get_json()
    
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    username = data.get('username', '').strip()
    password = data.get('password', '')
    
    if not username or not password:
        return jsonify({'success': False, 'message': 'Username dan password harus diisi'}), 400
    
    user = User.query.filter_by(username=username).first()
    
    if not user or not check_password_hash(user.password, password):
        return jsonify({'success': False, 'message': 'Username atau password salah'}), 401
    
    token = jwt.encode({
        'user_id': user.id,
        'username': user.username,
        'role': user.role,
        'exp': datetime.utcnow() + timedelta(days=1)
    }, app.config['SECRET_KEY'], algorithm="HS256")
    
    logger.info(f"User {username} logged in successfully")
    
    return jsonify({
        'success': True,
        'message': 'Login berhasil',
        'data': {
            'token': token,
            'user': user.to_dict()
        }
    })

@app.route('/api/auth/verify', methods=['GET'])
@token_required
def verify_token(current_user):
    return jsonify({
        'success': True,
        'data': current_user.to_dict()
    })

# ==================== CAMERA ROUTES ====================
@app.route('/api/cameras', methods=['GET'])
@token_required
@handle_db_error
def get_cameras(current_user):
    cameras = Camera.query.order_by(Camera.created_at.desc()).all()
    return jsonify({
        'success': True,
        'data': [camera.to_dict() for camera in cameras]
    })

@app.route('/api/cameras', methods=['POST'])
@token_required
@admin_required
@handle_db_error
def create_camera(current_user):
    data = request.get_json()
    
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    name = data.get('name', '').strip()
    rtsp_url = data.get('rtsp_url', '').strip()
    location = data.get('location', '').strip()
    
    if not name or not rtsp_url:
        return jsonify({'success': False, 'message': 'Nama dan RTSP URL harus diisi'}), 400
    
    is_valid, error_msg = validate_rtsp_url(rtsp_url)
    if not is_valid:
        return jsonify({'success': False, 'message': error_msg}), 400
    
    camera = Camera(
        name=name,
        rtsp_url=rtsp_url,
        location=location
    )
    db.session.add(camera)
    db.session.commit()
    
    logger.info(f"Camera created: {name} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'Kamera berhasil ditambahkan',
        'data': camera.to_dict()
    }), 201

@app.route('/api/cameras/<int:camera_id>', methods=['PUT'])
@token_required
@admin_required
@handle_db_error
def update_camera(current_user, camera_id):
    camera = db.session.get(Camera, camera_id)
    if not camera:
        return jsonify({'success': False, 'message': 'Kamera tidak ditemukan'}), 404
    
    # TAMBAHKAN INI: Cek apakah kamera sedang monitoring
    if camera.is_monitoring:
        return jsonify({
            'success': False, 
            'message': 'Tidak dapat edit kamera yang sedang monitoring. Stop monitoring terlebih dahulu.'
        }), 400
    
    data = request.get_json()
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    name = data.get('name', '').strip()
    rtsp_url = data.get('rtsp_url', '').strip()
    location = data.get('location', '').strip()
    
    if not name or not rtsp_url:
        return jsonify({'success': False, 'message': 'Nama dan RTSP URL harus diisi'}), 400
    
    is_valid, error_msg = validate_rtsp_url(rtsp_url)
    if not is_valid:
        return jsonify({'success': False, 'message': error_msg}), 400
    
    camera.name = name
    camera.rtsp_url = rtsp_url
    camera.location = location
    camera.updated_at = datetime.utcnow()
    db.session.commit()
    
    logger.info(f"Camera updated: {name} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'Kamera berhasil diupdate',
        'data': camera.to_dict()
    })

@app.route('/api/cameras/<int:camera_id>', methods=['DELETE'])
@token_required
@admin_required
@handle_db_error
def delete_camera(current_user, camera_id):
    camera = db.session.get(Camera, camera_id)
    if not camera:
        return jsonify({'success': False, 'message': 'Kamera tidak ditemukan'}), 404
    
    # TAMBAHKAN INI: Cek apakah kamera sedang monitoring
    if camera.is_monitoring:
        return jsonify({
            'success': False, 
            'message': 'Tidak dapat hapus kamera yang sedang monitoring. Stop monitoring terlebih dahulu.'
        }), 400
    
    # Hapus dari active streams jika ada
    if camera_id in active_streams:
        del active_streams[camera_id]
    
    camera_name = camera.name
    db.session.delete(camera)
    db.session.commit()
    
    logger.info(f"Camera deleted: {camera_name} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'Kamera berhasil dihapus'
    })

@app.route('/api/cameras/<int:camera_id>/start', methods=['POST'])
@token_required
@handle_db_error
def start_camera_monitoring(current_user, camera_id):
    # Cek batas concurrent cameras
    '''
    with streams_lock:
        active_count = len(active_streams)
        if active_count >= MAX_CONCURRENT_CAMERAS:
            return jsonify({
                'success': False, 
                'message': f'Maksimal {MAX_CONCURRENT_CAMERAS} kamera dapat berjalan bersamaan. Stop kamera lain terlebih dahulu.'
            }), 400
    '''

    # Cek setting dari database
    setting = SystemSetting.query.first()
    
    if setting and setting.enable_limit and setting.max_cameras > 0:
        with streams_lock:
            active_count = len(active_streams)
            
            if active_count >= setting.max_cameras:
                return jsonify({
                    'success': False, 
                    'message': f'Maksimal {setting.max_cameras} kamera dapat berjalan bersamaan. Stop kamera lain terlebih dahulu.'
                }), 400

    
    camera = db.session.get(Camera, camera_id)
    if not camera:
        return jsonify({'success': False, 'message': 'Kamera tidak ditemukan'}), 404
    
    if camera_id in active_streams:
        return jsonify({'success': False, 'message': 'Monitoring sudah berjalan'}), 400
    
    if reader is None:
        return jsonify({'success': False, 'message': 'EasyOCR belum siap, silakan restart server'}), 503
    
    # Set status ke "connecting" dulu
    camera.status = 'connecting'
    camera.is_monitoring = True
    db.session.commit()
    
    with streams_lock:
        active_streams[camera_id] = True
    thread = threading.Thread(target=monitor_camera_thread, args=(camera_id,), daemon=True)
    monitoring_threads[camera_id] = thread
    thread.start()
    
    logger.info(f"Monitoring started for camera {camera.name} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': f'Monitoring dimulai untuk kamera {camera.name}'
    })

@app.route('/api/cameras/<int:camera_id>/stop', methods=['POST'])
@token_required
@handle_db_error
def stop_camera_monitoring(current_user, camera_id):
    camera = db.session.get(Camera, camera_id)
    if not camera:
        return jsonify({'success': False, 'message': 'Kamera tidak ditemukan'}), 404
    
    if camera_id not in active_streams:
        return jsonify({'success': False, 'message': 'Monitoring tidak berjalan'}), 400
    
    with streams_lock:
        if camera_id in active_streams:
            del active_streams[camera_id]
    
    # Clear votes
    with votes_lock:
        if camera_id in plate_votes:
            del plate_votes[camera_id]
    
    camera.is_monitoring = False
    camera.status = 'offline'
    db.session.commit()
    
    logger.info(f"Monitoring stopped for camera {camera.name} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': f'Monitoring dihentikan untuk kamera {camera.name}'
    })

@app.route('/api/cameras/<int:camera_id>/stream')
def stream_camera(camera_id):
    """
    Stream video dari kamera (untuk popup viewer saja)
    Terpisah dari monitoring thread - popup ditutup tidak pengaruhi monitoring background
    """
    
    # Get token from query parameter (since img tag can't send headers)
    token = request.args.get('token')
    
    if not token:
        return jsonify({'success': False, 'message': 'Token diperlukan'}), 401
    
    try:
        data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
        current_user = db.session.get(User, data['user_id'])
        
        if not current_user:
            return jsonify({'success': False, 'message': 'User tidak ditemukan'}), 401
    except jwt.ExpiredSignatureError:
        return jsonify({'success': False, 'message': 'Token expired'}), 401
    except jwt.InvalidTokenError:
        return jsonify({'success': False, 'message': 'Token tidak valid'}), 401
    except Exception as e:
        logger.error(f"Token validation error: {str(e)}")
        return jsonify({'success': False, 'message': 'Autentikasi gagal'}), 401
    
    # Verify camera exists
    camera = db.session.get(Camera, camera_id)
    if not camera:
        return jsonify({'success': False, 'message': 'Kamera tidak ditemukan'}), 404
    
    def generate_frames():
        """
        Generate video frames from RTSP stream
        Ini hanya untuk viewer popup - TIDAK untuk monitoring/deteksi plat
        """
        cap = None
        consecutive_errors = 0
        max_errors = 3
        
        try:
            cap = cv2.VideoCapture(camera.rtsp_url)
            cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
            cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000)
            cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 3000)
            
            if not cap.isOpened():
                # Silent exit - popup mungkin sudah ditutup
                return
            
            while True:
                ret, frame = cap.read()
                
                if not ret:
                    consecutive_errors += 1
                    
                    if consecutive_errors >= max_errors:
                        # Silent exit - tidak perlu log, ini viewer saja
                        break
                    
                    time.sleep(0.1)
                    continue
                
                # Reset error counter
                consecutive_errors = 0
                
                # Resize frame untuk streaming (lebih ringan)
                frame = cv2.resize(frame, (640, 480))
                
                # Encode frame as JPEG
                ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 70])
                if not ret:
                    continue
                
                frame_bytes = buffer.tobytes()
                
                # Yield frame in multipart format
                yield (b'--frame\r\n'
                       b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
                
                # Limit frame rate to ~30 FPS
                time.sleep(0.033)
                
        except GeneratorExit:
            # Client disconnected (popup closed) - ini normal, tidak perlu log
            pass
        except Exception as e:
            logger.debug(f"Stream error (Camera {camera_id}): {str(e)}")
            pass
        finally:
            # Always cleanup
            if cap is not None:
                cap.release()
    
    return Response(
        generate_frames(),
        mimetype='multipart/x-mixed-replace; boundary=frame'
    )

def create_error_frame(message):
    """Create error placeholder image"""
    import numpy as np
    
    # Create black image 640x480
    frame = np.zeros((480, 640, 3), dtype=np.uint8)
    
    # Add text
    font = cv2.FONT_HERSHEY_SIMPLEX
    text_size = cv2.getTextSize(message, font, 0.7, 2)[0]
    text_x = (640 - text_size[0]) // 2
    text_y = (480 + text_size[1]) // 2
    
    cv2.putText(frame, message, (text_x, text_y), font, 0.7, (255, 255, 255), 2)
    
    return frame

# ==================== PLATE ROUTES ====================

@app.route('/api/plates', methods=['GET'])
@token_required
@handle_db_error
def get_plate_records(current_user):
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    camera_id = request.args.get('camera_id', type=int)
    search = request.args.get('search', '').strip()
    
    if per_page > 100:
        per_page = 100
    
    query = PlateRecord.query
    
    if camera_id:
        query = query.filter_by(camera_id=camera_id)
    
    if search:
        query = query.filter(PlateRecord.plate_number.like(f'%{search}%'))
    
    pagination = query.order_by(PlateRecord.detected_at.desc()).paginate(
        page=page, per_page=per_page, error_out=False
    )
    
    return jsonify({
        'success': True,
        'data': {
            'records': [record.to_dict() for record in pagination.items],
            'total': pagination.total,
            'pages': pagination.pages,
            'current_page': page,
            'per_page': per_page
        }
    })

@app.route('/api/plates/<int:plate_id>', methods=['DELETE'])
@token_required
@admin_required
@handle_db_error
def delete_plate(current_user, plate_id):
    plate = db.session.get(PlateRecord, plate_id)
    if not plate:
        return jsonify({'success': False, 'message': 'Record tidak ditemukan'}), 404
    
    plate_number = plate.plate_number
    db.session.delete(plate)
    db.session.commit()
    
    logger.info(f"Plate record deleted: {plate_number} by {current_user.username}")
    
    # FIXED: Jangan stop kamera saat hapus record manual
    # Camera monitoring tetap jalan terlepas dari jumlah records
    return jsonify({
        'success': True,
        'message': 'Record berhasil dihapus'
    })
    
# ==================== USER ROUTES ====================

@app.route('/api/users', methods=['GET'])
@token_required
@admin_required
@handle_db_error
def get_users(current_user):
    users = User.query.order_by(User.created_at.desc()).all()
    return jsonify({
        'success': True,
        'data': [user.to_dict() for user in users]
    })

@app.route('/api/users', methods=['POST'])
@token_required
@admin_required
@handle_db_error
def create_user(current_user):
    data = request.get_json()
    
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    username = data.get('username', '').strip()
    password = data.get('password', '')
    role = data.get('role', 'user')
    
    if not username or not password:
        return jsonify({'success': False, 'message': 'Username dan password harus diisi'}), 400
    
    if len(password) < 6:
        return jsonify({'success': False, 'message': 'Password minimal 6 karakter'}), 400
    
    if role not in ['admin', 'user']:
        return jsonify({'success': False, 'message': 'Role tidak valid'}), 400
    
    existing_user = User.query.filter_by(username=username).first()
    if existing_user:
        return jsonify({'success': False, 'message': 'Username sudah digunakan'}), 400
    
    user = User(
        username=username,
        password=generate_password_hash(password),
        role=role
    )
    db.session.add(user)
    db.session.commit()
    
    logger.info(f"User created: {username} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'User berhasil ditambahkan',
        'data': user.to_dict()
    }), 201

@app.route('/api/users/<int:user_id>', methods=['PUT'])
@token_required
@admin_required
@handle_db_error
def update_user(current_user, user_id):
    user = db.session.get(User, user_id)
    if not user:
        return jsonify({'success': False, 'message': 'User tidak ditemukan'}), 404
    
    data = request.get_json()
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    username = data.get('username', '').strip()
    password = data.get('password', '')
    role = data.get('role', 'user')
    
    if not username:
        return jsonify({'success': False, 'message': 'Username harus diisi'}), 400
    
    if role not in ['admin', 'user']:
        return jsonify({'success': False, 'message': 'Role tidak valid'}), 400
    
    if username != user.username:
        existing_user = User.query.filter_by(username=username).first()
        if existing_user:
            return jsonify({'success': False, 'message': 'Username sudah digunakan'}), 400
    
    user.username = username
    user.role = role
    
    if password:
        if len(password) < 6:
            return jsonify({'success': False, 'message': 'Password minimal 6 karakter'}), 400
        user.password = generate_password_hash(password)
    
    user.updated_at = datetime.utcnow()
    db.session.commit()
    
    logger.info(f"User updated: {username} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'User berhasil diupdate',
        'data': user.to_dict()
    })

@app.route('/api/users/<int:user_id>', methods=['DELETE'])
@token_required
@admin_required
@handle_db_error
def delete_user(current_user, user_id):
    if current_user.id == user_id:
        return jsonify({'success': False, 'message': 'Tidak dapat menghapus akun sendiri'}), 400
    
    user = db.session.get(User, user_id)
    if not user:
        return jsonify({'success': False, 'message': 'User tidak ditemukan'}), 404
    
    username = user.username
    db.session.delete(user)
    db.session.commit()
    
    logger.info(f"User deleted: {username} by {current_user.username}")
    
    return jsonify({
        'success': True,
        'message': 'User berhasil dihapus'
    })

# ==================== DASHBOARD ROUTES ====================

@app.route('/api/dashboard', methods=['GET'])
@token_required
@handle_db_error
def get_dashboard(current_user):
    total_cameras = Camera.query.count()
    online_cameras = Camera.query.filter_by(status='online').count()
    total_users = User.query.count()
    total_detections = PlateRecord.query.count()
    
    today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
    today_detections = PlateRecord.query.filter(PlateRecord.detected_at >= today_start).count()
    
    return jsonify({
        'success': True,
        'data': {
            'total_cameras': total_cameras,
            'online_cameras': online_cameras,
            'total_users': total_users,
            'total_detections': total_detections,
            'today_detections': today_detections
        }
    })

# ==================== AUTO DELETE ROUTES ====================

@app.route('/api/settings/camera-limit', methods=['GET'])
@token_required
@handle_db_error
def get_camera_limit(current_user):
    setting = SystemSetting.query.first()
    if not setting:
        setting = SystemSetting(max_cameras=5, enable_limit=True)
        db.session.add(setting)
        db.session.commit()
    
    return jsonify({
        'success': True,
        'data': setting.to_dict()
    })

@app.route('/api/settings/camera-limit', methods=['PUT'])
@token_required
@admin_required
@handle_db_error
def update_camera_limit(current_user):
    data = request.get_json()
    
    setting = SystemSetting.query.first()
    if not setting:
        setting = SystemSetting()
        db.session.add(setting)
    
    max_cameras = data.get('max_cameras', 5)
    enable_limit = data.get('enable_limit', True)
    
    if max_cameras < 0:
        return jsonify({'success': False, 'message': 'Limit tidak boleh negatif'}), 400
    
    setting.max_cameras = max_cameras
    setting.enable_limit = enable_limit
    setting.updated_at = datetime.utcnow()
    db.session.commit()
    
    logger.info(f"Camera limit updated by {current_user.username}: max={max_cameras}, enabled={enable_limit}")
    
    return jsonify({
        'success': True,
        'message': 'Setting berhasil diupdate',
        'data': setting.to_dict()
    })

@app.route('/api/settings/auto-delete', methods=['GET'])
@token_required
@handle_db_error
def get_auto_delete_setting(current_user):
    setting = AutoDeleteSetting.query.first()
    if not setting:
        setting = AutoDeleteSetting(is_enabled=False, delete_after_days=7)
        db.session.add(setting)
        db.session.commit()
    
    return jsonify({
        'success': True,
        'data': setting.to_dict()
    })

@app.route('/api/settings/auto-delete', methods=['PUT'])
@token_required
@admin_required
@handle_db_error
def update_auto_delete_setting(current_user):
    data = request.get_json()
    
    if not data:
        return jsonify({'success': False, 'message': 'Data tidak valid'}), 400
    
    setting = AutoDeleteSetting.query.first()
    if not setting:
        setting = AutoDeleteSetting()
        db.session.add(setting)
    
    is_enabled = data.get('is_enabled', False)
    delete_after_days = data.get('delete_after_days', 7)
    
    if delete_after_days not in [7, 30, 90]:
        return jsonify({'success': False, 'message': 'Periode harus 7, 30, atau 90 hari'}), 400
    
    setting.is_enabled = is_enabled
    setting.delete_after_days = delete_after_days
    setting.updated_at = datetime.utcnow()
    db.session.commit()
    
    logger.info(f"Auto-delete setting updated by {current_user.username}: enabled={is_enabled}, days={delete_after_days}")
    
    return jsonify({
        'success': True,
        'message': 'Setting berhasil diupdate',
        'data': setting.to_dict()
    })

@app.route('/api/settings/auto-delete/cleanup', methods=['POST'])
@token_required
@admin_required
@handle_db_error
def manual_cleanup(current_user):
    setting = AutoDeleteSetting.query.first()
    if not setting:
        return jsonify({'success': False, 'message': 'Setting belum dikonfigurasi'}), 400
    
    if not setting.is_enabled:
        return jsonify({'success': False, 'message': 'Auto-delete tidak aktif'}), 400
    
    cutoff_date = datetime.utcnow() - timedelta(days=setting.delete_after_days)
    deleted_count = PlateRecord.query.filter(PlateRecord.detected_at < cutoff_date).delete()
    
    setting.last_cleanup = datetime.utcnow()
    db.session.commit()
    
    logger.info(f"Manual cleanup by {current_user.username}: deleted {deleted_count} records")
    
    # FIXED: Cleanup tidak menghentikan monitoring kamera
    # Auto-delete hanya membersihkan database, kamera tetap monitoring
    return jsonify({
        'success': True,
        'message': f'Berhasil menghapus {deleted_count} record lama',
        'data': {
            'deleted_count': deleted_count,
            'cutoff_date': cutoff_date.strftime('%Y-%m-%d %H:%M:%S')
        }
    })

@app.route('/api/settings/delete-all-plates', methods=['POST'])
@token_required
@admin_required
@handle_db_error
def delete_all_plates(current_user):
    """Hapus SEMUA data plat nomor saja (tidak termasuk kamera dan user)"""
    if not request.json or not request.json.get('confirm'):
        return jsonify({'success': False, 'message': 'Konfirmasi diperlukan'}), 400
    
    try:
        # Hanya hapus data di tabel PlateRecord
        total_count = PlateRecord.query.count()
        PlateRecord.query.delete()
        db.session.commit()
        
        logger.info(f"All plate records deleted by {current_user.username}: {total_count} records")
        
        return jsonify({
            'success': True,
            'message': f'Berhasil menghapus semua {total_count} record plat nomor',
            'data': {'deleted_count': total_count}
        })
    except Exception as e:
        db.session.rollback()
        logger.error(f"Error deleting all plate records: {str(e)}")
        return jsonify({'success': False, 'message': f'Gagal menghapus data: {str(e)}'}), 500

# ==================== EXPORT ROUTES ====================

@app.route('/api/plates/export', methods=['GET'])
@token_required
@handle_db_error
def export_plates(current_user):
    import csv
    from io import StringIO
    
    camera_id = request.args.get('camera_id', type=int)
    search = request.args.get('search', '').strip()
    
    query = PlateRecord.query
    
    if camera_id:
        query = query.filter_by(camera_id=camera_id)
    
    if search:
        query = query.filter(PlateRecord.plate_number.like(f'%{search}%'))
    
    records = query.order_by(PlateRecord.detected_at.desc()).all()
    
    output = StringIO()
    writer = csv.writer(output)
    
    writer.writerow(['No', 'Plat Nomor', 'Kamera', 'Lokasi', 'Confidence (%)', 'Waktu Deteksi'])
    
    for idx, record in enumerate(records, 1):
        writer.writerow([
            idx,
            record.plate_number,
            record.camera.name,
            record.camera.location or '-',
            round(record.confidence * 100, 2) if record.confidence else 0,
            record.detected_at.strftime('%Y-%m-%d %H:%M:%S')
        ])
    
    output.seek(0)
    
    return Response(
        output.getvalue(),
        mimetype='text/csv',
        headers={
            'Content-Disposition': f'attachment; filename=plate_records_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
        }
    )

# ==================== STATIC ROUTES ====================

@app.route('/')
def index():
    return send_file('index.html')

@app.route('/favicon.ico')
def favicon():
    return '', 204

# ==================== DATABASE INITIALIZATION ====================

def init_database():
    with app.app_context():
        try:
            db.create_all()
            logger.info("Database tables created successfully!")
            
            admin = User.query.filter_by(username='admin').first()
            if not admin:
                admin = User(
                    username='admin',
                    password=generate_password_hash('admin123'),
                    role='admin'
                )
                db.session.add(admin)
            
            setting = AutoDeleteSetting.query.first()
            if not setting:
                setting = AutoDeleteSetting(
                    is_enabled=False,
                    delete_after_days=7
                )
                db.session.add(setting)
            
            sys_setting = SystemSetting.query.first()
            if not sys_setting:
                sys_setting = SystemSetting(
                    max_cameras=0,  # 0 = unlimited
                    enable_limit=False
                )
                db.session.add(sys_setting)
            
            db.session.commit()
            logger.info("Default admin created: username=admin, password=admin123")
            
            print("\n" + "="*60)
            print("SISTEM ANPR SIAP DIGUNAKAN! (OPTIMIZED VERSION)")
            print("="*60)
            print(f"Database: {DB_NAME}")
            print(f"Admin: admin / admin123")
            print(f"Server: http://0.0.0.0:5000")
            print(f"GPU: {'Enabled' if USE_GPU else 'Disabled (CPU only)'}")
            print(f"Detection Interval: {DETECTION_INTERVAL} frames (~{DETECTION_INTERVAL/30:.1f}s)")
            print(f"Frame Resize: {FRAME_RESIZE}")
            print("="*60 + "\n")
            
        except Exception as e:
            logger.error(f"ERROR INITIALIZING DATABASE: {str(e)}")
            print(f"\nERROR: {str(e)}\n")
            exit(1)

def restore_monitoring():
    """Restore kamera yang masih online/is_monitoring ketika server restart"""
    with app.app_context():
        cameras = Camera.query.filter_by(is_monitoring=True, status='online').all()
        for camera in cameras:
            if camera.id not in active_streams:
                active_streams[camera.id] = True
                thread = threading.Thread(target=monitor_camera_thread, args=(camera.id,), daemon=True)
                monitoring_threads[camera.id] = thread
                thread.start()
                logger.info(f"Restored monitoring for camera {camera.name} (ID: {camera.id})")

if __name__ == '__main__':
    init_database()
    restore_monitoring()
    
    # Gunakan environment variable untuk debug mode
    debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() == 'true'
    
    if debug_mode:
        logger.warning("Running in DEBUG mode - DO NOT use in production!")
    
    app.run(debug=debug_mode, host='0.0.0.0', port=5000, threaded=True)