#!/usr/bin/env python3
"""
ADMS Local Sync Agent

Syncs user data from cloud ADMS to local ZKTeco biometric devices.
Runs as a background service and periodically fetches user data from cloud
and distributes it to all local devices on the network.

Configuration:
- Set CLOUD_URL environment variable or in config file
- Set SYNC_INTERVAL in config (default: 10 minutes)
- Set device IP addresses in config or auto-discover

Usage:
    python3 sync_agent.py
    python3 sync_agent.py --config /path/to/config.json
    python3 sync_agent.py --device 192.168.1.100  (test single device)
"""

import os
import sys
import json
import requests
import socket
import logging
import time
from datetime import datetime
from typing import List, Dict, Optional, Tuple
from pathlib import Path
import argparse

try:
    from apscheduler.schedulers.background import BackgroundScheduler
    from apscheduler.triggers.interval import IntervalTrigger
except ImportError:
    print("ERROR: apscheduler not installed. Run: pip install -r requirements.txt")
    sys.exit(1)


# ============================================================================
# LOGGING SETUP
# ============================================================================

def setup_logging():
    """Configure logging"""
    log_dir = Path('logs')
    log_dir.mkdir(exist_ok=True)
    
    log_file = log_dir / f"sync_agent_{datetime.now().strftime('%Y-%m-%d')}.log"
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s [%(levelname)s] %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        handlers=[
            logging.FileHandler(log_file),
            logging.StreamHandler(),
        ]
    )
    
    return logging.getLogger(__name__)

logger = setup_logging()


# ============================================================================
# CONFIGURATION
# ============================================================================

class Config:
    """Configuration management"""
    
    def __init__(self, config_file: Optional[str] = None):
        self.config_file = config_file or 'sync_agent_config.json'
        self.data = self._load_config()
    
    def _load_config(self) -> Dict:
        """Load configuration from file or environment"""
        
        # Default config
        default_config = {
            'cloud_url': os.getenv('ADMS_CLOUD_URL', 'http://localhost'),
            'sync_interval_minutes': 10,
            'device_timeout_seconds': 30,
            'max_retries': 3,
            'devices': [],  # or list of IPs
            'auto_discover': True,  # auto-discover devices on network
            'network_prefix': '192.168.1',  # for auto-discovery
            'port': 4370,  # ZKTeco port
        }
        
        # Try to load from file
        if Path(self.config_file).exists():
            try:
                with open(self.config_file, 'r') as f:
                    file_config = json.load(f)
                default_config.update(file_config)
                logger.info(f"✓ Loaded configuration from {self.config_file}")
            except Exception as e:
                logger.warning(f"Could not load config file: {e}. Using defaults.")
        
        # Override with environment variables
        default_config['cloud_url'] = os.getenv('ADMS_CLOUD_URL', default_config['cloud_url'])
        default_config['sync_interval_minutes'] = int(os.getenv('SYNC_INTERVAL', default_config['sync_interval_minutes']))
        
        return default_config
    
    def get(self, key: str, default=None):
        """Get config value"""
        return self.data.get(key, default)
    
    def save_template(self):
        """Create a template config file"""
        template = {
            "cloud_url": "https://your-adms.cloud.com",
            "sync_interval_minutes": 10,
            "device_timeout_seconds": 30,
            "max_retries": 3,
            "devices": [
                "192.168.1.100",
                {
                    "ip": "192.168.1.101",
                    "password": "device_password_here"
                },
                {
                    "ip": "192.168.1.102",
                    "password": "another_device_password"
                }
            ],
            "auto_discover": True,
            "network_prefix": "192.168.1",
            "port": 4370,
            "notes": "Edit this file and set cloud_url. For devices with password, use object format: {\"ip\": \"...\", \"password\": \"...\"}"
        }
        
        with open('sync_agent_config.json', 'w') as f:
            json.dump(template, f, indent=2)
        
        logger.info("✓ Created sync_agent_config.json template. Please edit it.")


# ============================================================================
# CLOUD API CLIENT
# ============================================================================

class CloudAPIClient:
    """Client for communicating with cloud ADMS"""
    
    def __init__(self, base_url: str, timeout: int = 10):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
    
    def _request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict]:
        """Make HTTP request"""
        url = f"{self.base_url}/api{endpoint}"
        
        try:
            response = requests.request(
                method,
                url,
                timeout=self.timeout,
                **kwargs
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            logger.error(f"❌ API request failed: {e}")
            return None
    
    def get_users(self) -> Optional[List[Dict]]:
        """Get all users from cloud"""
        result = self._request('GET', '/sync/users')
        if result and result.get('success'):
            return result.get('users', [])
        return None
    
    def get_devices(self) -> Optional[List[Dict]]:
        """Get all devices from cloud"""
        result = self._request('GET', '/sync/devices')
        if result and result.get('success'):
            return result.get('devices', [])
        return None
    
    def get_sync_status(self) -> Optional[Dict]:
        """Get sync status"""
        result = self._request('GET', '/sync/status')
        if result and result.get('success'):
            return result.get('status', {})
        return None
    
    def report_sync(self, report: Dict) -> bool:
        """Report sync completion"""
        result = self._request('POST', '/sync/report', json=report)
        return result and result.get('success', False)
    
    def health_check(self) -> bool:
        """Check cloud API health"""
        result = self._request('GET', '/sync/health')
        return result and result.get('success', False)


# ============================================================================
# DEVICE DISCOVERY & COMMUNICATION
# ============================================================================

class DeviceDiscovery:
    """Discover ZKTeco devices on local network"""
    
    @staticmethod
    def ping(ip: str, timeout: int = 2) -> bool:
        """Check if device is reachable"""
        try:
            sock = socket.create_connection((ip, 80), timeout=timeout)
            sock.close()
            return True
        except (socket.timeout, socket.error):
            return False
    
    @staticmethod
    def auto_discover(network_prefix: str = '192.168.1', max_hosts: int = 254) -> List[str]:
        """Auto-discover devices on network"""
        devices = []
        logger.info(f"🔍 Auto-discovering devices on {network_prefix}.0/24...")
        
        for i in range(1, min(max_hosts + 1, 255)):
            ip = f"{network_prefix}.{i}"
            if DeviceDiscovery.ping(ip, timeout=1):
                devices.append(ip)
                logger.info(f"   ✓ Found device: {ip}")
        
        logger.info(f"✓ Discovery complete. Found {len(devices)} devices.")
        return devices


class DeviceSync:
    """Sync user data to ZKTeco device"""
    
    def __init__(self, device_ip: str, port: int = 4370, timeout: int = 30, password: Optional[str] = None):
        self.device_ip = device_ip
        self.port = port
        self.timeout = timeout
        self.password = password
    
    def sync_users(self, users: List[Dict]) -> Tuple[bool, str]:
        """Sync users to device via HTTP"""
        try:
            # Format user data
            user_data = {
                'users': [
                    {
                        'id': u['id'],
                        'name': u.get('name', 'Unknown'),
                        'user_id': u.get('nip') or str(u['id']),
                        'email': u.get('email', ''),
                    }
                    for u in users
                ]
            }
            
            # Add password if provided
            if self.password:
                user_data['password'] = self.password
            
            # Send to device
            url = f"http://{self.device_ip}:{self.port}/iclock/user"
            response = requests.post(
                url,
                json=user_data,
                timeout=self.timeout
            )
            
            if response.status_code == 200:
                return True, f"Successfully synced {len(users)} users"
            else:
                return False, f"HTTP {response.status_code}: {response.text}"
        
        except requests.exceptions.Timeout:
            return False, "Device timeout - check connectivity"
        except requests.exceptions.ConnectionError:
            return False, "Cannot connect to device"
        except Exception as e:
            return False, str(e)


# ============================================================================
# SYNC AGENT
# ============================================================================

class SyncAgent:
    """Main sync agent"""
    
    def __init__(self, config: Config):
        self.config = config
        self.cloud = CloudAPIClient(config.get('cloud_url'))
        self.scheduler = BackgroundScheduler()
        self.sync_count = 0
        self.last_error = None
    
    def sync_cycle(self):
        """Main sync cycle"""
        try:
            logger.info("=" * 80)
            logger.info("🔄 STARTING SYNC CYCLE")
            logger.info("=" * 80)
            
            sync_start_time = time.time()
            
            # Step 1: Check cloud connectivity
            if not self.cloud.health_check():
                raise Exception("Cloud API is unreachable")
            
            # Step 2: Fetch users from cloud
            users = self.cloud.get_users()
            if users is None:
                raise Exception("Failed to fetch users from cloud")
            
            logger.info(f"📥 Fetched {len(users)} users from cloud")
            
            if not users:
                logger.warning("⚠️  No users to sync")
                return
            
            # Step 3: Get device list
            devices_from_cloud = self.cloud.get_devices()
            if devices_from_cloud is None:
                devices_from_cloud = []
            
            # Step 4: Determine devices to sync
            devices_to_sync = self.config.get('devices', [])
            
            if not devices_to_sync and self.config.get('auto_discover'):
                logger.info("🔍 Auto-discovering devices...")
                devices_to_sync = DeviceDiscovery.auto_discover(
                    self.config.get('network_prefix'),
                    max_hosts=254
                )
            
            if not devices_to_sync:
                logger.warning("⚠️  No devices found to sync")
                return
            
            logger.info(f"📡 Syncing to {len(devices_to_sync)} device(s)")
            
            # Step 5: Sync to each device
            successful = 0
            failed = 0
            
            for device_entry in devices_to_sync:
                # Handle both simple IP strings and device objects
                if isinstance(device_entry, dict):
                    device_ip = device_entry.get('ip')
                    device_password = device_entry.get('password')
                else:
                    device_ip = device_entry
                    device_password = None
                
                logger.info(f"\n   Syncing to {device_ip}...")
                
                device = DeviceSync(
                    device_ip, 
                    self.config.get('port'), 
                    self.config.get('device_timeout_seconds'),
                    password=device_password
                )
                success, message = device.sync_users(users)
                
                if success:
                    logger.info(f"   ✅ {message}")
                    successful += 1
                    
                    # Find device ID from cloud config
                    device_id = None
                    for d in devices_from_cloud:
                        if d.get('ip_device') == device_ip:
                            device_id = d.get('id')
                            break
                    
                    # Report success to cloud
                    if device_id:
                        report = {
                            'device_id': device_id,
                            'device_sn': next((d.get('no_sn') for d in devices_from_cloud if d.get('ip_device') == device_ip), 'unknown'),
                            'status': 'success',
                            'users_synced': len(users),
                            'sync_time': time.time() - sync_start_time,
                            'timestamp': datetime.now().isoformat(),
                        }
                        
                        if self.cloud.report_sync(report):
                            logger.info(f"   📤 Reported sync status to cloud")
                else:
                    logger.error(f"   ❌ {message}")
                    failed += 1
            
            # Summary
            sync_duration = time.time() - sync_start_time
            logger.info("\n" + "=" * 80)
            logger.info(f"✨ SYNC CYCLE COMPLETE")
            logger.info(f"   ✅ Successful: {successful}")
            logger.info(f"   ❌ Failed: {failed}")
            logger.info(f"   ⏱️  Duration: {sync_duration:.1f}s")
            logger.info("=" * 80 + "\n")
            
            self.sync_count += 1
            self.last_error = None
        
        except Exception as e:
            logger.error(f"❌ Sync cycle failed: {e}")
            self.last_error = str(e)
    
    def start(self):
        """Start the agent"""
        try:
            interval = self.config.get('sync_interval_minutes', 10)
            
            logger.info(f"🚀 Starting sync agent")
            logger.info(f"   Cloud URL: {self.config.get('cloud_url')}")
            logger.info(f"   Sync interval: {interval} minutes")
            logger.info(f"   Device timeout: {self.config.get('device_timeout_seconds')}s")
            
            # Add job to scheduler
            self.scheduler.add_job(
                self.sync_cycle,
                trigger=IntervalTrigger(minutes=interval),
                id='sync_cycle',
                name='User Sync Cycle',
                replace_existing=True
            )
            
            # Run immediately on startup
            logger.info("🔄 Running initial sync cycle...")
            self.sync_cycle()
            
            # Start scheduler
            self.scheduler.start()
            logger.info("✅ Sync agent running. Press Ctrl+C to stop.\n")
            
            # Keep running
            try:
                while True:
                    time.sleep(1)
            except KeyboardInterrupt:
                logger.info("\n⏹️  Stopping sync agent...")
                self.scheduler.shutdown()
                logger.info("✓ Sync agent stopped")
        
        except Exception as e:
            logger.error(f"Failed to start sync agent: {e}")
            sys.exit(1)
    
    def test_device(self, device_ip: str):
        """Test sync to a single device"""
        logger.info(f"🧪 Testing sync to {device_ip}...")
        
        users = self.cloud.get_users()
        if not users:
            logger.error("Failed to fetch users from cloud")
            return
        
        device = DeviceSync(device_ip, self.config.get('port'))
        success, message = device.sync_users(users)
        
        if success:
            logger.info(f"✅ Success: {message}")
        else:
            logger.error(f"❌ Failed: {message}")


# ============================================================================
# MAIN
# ============================================================================

def main():
    parser = argparse.ArgumentParser(
        description='ADMS Local Sync Agent - Syncs users from cloud to local devices'
    )
    parser.add_argument(
        '--config',
        help='Path to configuration file',
        default='sync_agent_config.json'
    )
    parser.add_argument(
        '--device',
        help='Test sync to specific device IP'
    )
    parser.add_argument(
        '--create-config',
        action='store_true',
        help='Create configuration template'
    )
    
    args = parser.parse_args()
    
    # Create config
    config = Config(args.config)
    
    # Create config template if requested
    if args.create_config:
        config.save_template()
        return
    
    # Check if config is set up
    if not config.get('cloud_url') or config.get('cloud_url') == 'http://localhost':
        logger.error("❌ ADMS_CLOUD_URL not configured!")
        logger.error("   Set environment variable: export ADMS_CLOUD_URL=https://your-adms.cloud.com")
        logger.error("   Or edit sync_agent_config.json and set cloud_url")
        logger.error("   Run: python3 sync_agent.py --create-config")
        sys.exit(1)
    
    # Create agent
    agent = SyncAgent(config)
    
    # Test specific device if requested
    if args.device:
        agent.test_device(args.device)
        return
    
    # Start sync agent
    agent.start()


if __name__ == '__main__':
    main()
