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 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

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)  # ← Tambahkan ini untuk debug OCR

app = Flask(__name__)
CORS(app)

# ==================== KONFIGURASI ====================
# GUNAKAN ENVIRONMENT VARIABLES UNTUK PRODUCTION
DB_USER = os.getenv('DB_USER', 'gonit')
DB_PASS = os.getenv('DB_PASS', 'b0bc1b62')
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = os.getenv('DB_PORT', '3306')
DB_NAME = os.getenv('DB_NAME', 'anpr_db')
SECRET_KEY = os.getenv('SECRET_KEY', 'GANTI-INI-DENGAN-KEY-RANDOM-YANG-KUAT-DAN-UNIK')

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 = 15  # Deteksi setiap 15 frame (~0.5 detik di kamera 30 FPS)
FRAME_RESIZE = (640, 480)  # Resize frame untuk menghemat proses OCR
FPS_LIMIT = 0.05  # Sleep 0.05 detik = max ~20 FPS
DB_UPDATE_INTERVAL = 500  # Update database setiap 500 frame, bukan 100
USE_GPU = False  # Set True jika punya GPU NVIDIA dengan CUDA

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 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 = {}

# ==================== 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)
    suffix = text[last_digit_idx+1:]
    if len(suffix) < 1 or len(suffix) > 3:
        return False
    if not suffix.isalpha():
        return False
    
    # 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 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]
    """
    if reader is None:
        logger.error("EasyOCR not initialized")
        return None, 0

    try:
        # 1. RESIZE OPTIMAL
        frame_resized = cv2.resize(image, (1280, 720))  # Resolusi lebih tinggi
        h, w = frame_resized.shape[:2]
        
        # 2. ROI LEBIH KETAT - fokus area plat
        roi_y_start = int(h * 0.50)  # Lebih ke bawah
        roi_y_end = int(h * 0.80)     # Lebih pendek
        roi_x_start = int(w * 0.20)   # Lebih ke tengah
        roi_x_end = int(w * 0.80)
        roi = frame_resized[roi_y_start:roi_y_end, roi_x_start:roi_x_end]
        
        # 3. PREPROCESSING AGRESIF - 6 METHOD
        gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
        
        variants = []
        
        # Method A: CLAHE Extreme
        clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(4,4))
        enhanced = clahe.apply(gray)
        variants.append(("CLAHE", enhanced))
        
        # Method B: 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 C: Adaptive Gaussian
        adaptive = cv2.adaptiveThreshold(
            gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
            cv2.THRESH_BINARY, 11, 2
        )
        variants.append(("Adaptive", adaptive))
        
        # Method D: INVERTED (untuk plat hitam dengan tulisan putih)
        _, binary_inv = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
        variants.append(("Inverted", binary_inv))
        
        # Method E: Unsharp Mask (super sharp)
        gaussian = cv2.GaussianBlur(gray, (0, 0), 2.0)
        unsharp = cv2.addWeighted(gray, 2.0, gaussian, -1.0, 0)
        variants.append(("Unsharp", unsharp))
        
        # Method F: Simple Binary (baseline)
        _, simple = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        variants.append(("Simple", simple))
        
        # 4. 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,  # Lebih lebar lagi
                    text_threshold=0.2,  # Lebih rendah
                    low_text=0.05,
                    link_threshold=0.2,
                    canvas_size=3840,  # 4K internal
                    mag_ratio=2.0,  # Perbesar 2x
                    allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'  # Hanya huruf + angka
                )
                
                for bbox, text, conf in results:
                    all_results.append((bbox, text, conf, variant_name))
                    # LOG SEMUA RAW OCR RESULTS
                    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]{1,3}$'
        candidates = []
        
        for bbox, raw_text, conf, method in all_results:
            # Bersihkan text
            text = re.sub(r'[^A-Za-z0-9]', '', raw_text).upper()
            
            if len(text) < 5 or len(text) > 9:
                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
                })
        
        # 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))
        
        # 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 best['plate'], best['conf']
        else:
            logger.debug(f"❌ [Cam{camera_id}] No valid plate from {len(all_results)} OCR results")
            return None, 0

    except Exception as e:
        logger.error(f"[Cam{camera_id}] Exception: {str(e)}")
        import traceback
        logger.error(traceback.format_exc())
        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
    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])
    
    # 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)
            result.append(fix_to_digit(ch))
        else:
            # Section 3: Seri (huruf)
            result.append(fix_to_letter(ch))
    
    return ''.join(result)
    
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 monitor_camera_thread(camera_id):
    """Thread untuk memantau kamera dan mendeteksi plat secara otomatis"""
    
    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)
            
            # TAMBAHKAN: 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  # Naikkan threshold karena corrupt frame biasa terjadi

    while True:
        if camera_id not in active_streams:
            logger.info(f"[Camera {camera_id}] Monitoring stopped externally, exiting thread.")
            break

        ret, frame = cap.read()
        
        if not ret or frame is None:
            consecutive_failures += 1
            
            # UBAH: Jangan log setiap kali, hanya setiap 10 kali
            if consecutive_failures % 10 == 0:
                logger.warning(f"[Camera {camera_id}] Frame read failed {consecutive_failures} times (likely corrupt frames)")
            
            # 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")
                    
                    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
            
            # PENTING: Jangan sleep lama, langsung skip ke frame berikutnya
            time.sleep(0.01)  # 10ms saja
            continue

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

        # Proses OCR setiap 15 frame (~0.5 detik)
        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)

            if plate_text:
                with app.app_context():
                    try:
                        logger.info(f"[DEBUG] Attempting to save {plate_text} (conf={conf:.2f}) to DB...")

                        recent = (
                            PlateRecord.query 
                            .filter_by(plate_number=plate_text)
                            .order_by(PlateRecord.detected_at.desc()) 
                            .first()
                        )

                        if not recent or (datetime.utcnow() - recent.detected_at).total_seconds() > 10: 
                            new_plate = PlateRecord( 
                                plate_number=plate_text,
                                confidence=conf,
                                camera_id=camera_id,
                                detected_at=datetime.utcnow() 
                            )

                            db.session.add(new_plate)
                            db.session.commit()

                            logger.info(f"✅ Plate {plate_text} saved to database (Camera {camera_id})")

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

                    except Exception as e:
                        db.session.rollback()
                        logger.error(f"❌ Failed to save plate {plate_text}: {str(e)}")

        time.sleep(FPS_LIMIT) 

    # Cleanup
    if cap:
        cap.release()
    
    with app.app_context():
        camera = db.session.get(Camera, camera_id)
        if camera:
            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):
    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()
    
    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
    
    del active_streams[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:
            # Silent - stream viewer error tidak perlu di-log
            # Monitoring thread yang terpisah tetap jalan normal
            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/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')
        }
    })

# ==================== 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)
            
            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()
    app.run(debug=True, host='0.0.0.0', port=5000, threaded=True)