"""
Scheduler service for managing automated product scraping.
Handles job scheduling, execution, and configuration updates.
"""
import logging
from datetime import datetime
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from typing import Optional, Dict, Any
import pytz

from app.models.site_settings import SiteSettings
from app.models.product import Product
from app.services.scraper_service import ScraperService
from app.db.session import AsyncSessionLocal

logger = logging.getLogger(__name__)


class SchedulerService:
    """Service for managing automated scraping scheduler"""
    
    _scheduler: Optional[AsyncIOScheduler] = None
    _job_id = "scheduled_scraping_job"
    
    @classmethod
    def get_scheduler(cls) -> AsyncIOScheduler:
        """Get or create the scheduler instance (in-memory)"""
        if cls._scheduler is None:
            cls._scheduler = AsyncIOScheduler()
            logger.info("[Scheduler] Created in-memory async scheduler (jobs reload from DB on startup)")
        return cls._scheduler
    
    @classmethod
    async def start_scheduler(cls):
        """Start the scheduler"""
        try:
            scheduler = cls.get_scheduler()
            if not scheduler.running:
                scheduler.start()
                logger.info("✅ [Scheduler] Scheduler started successfully")
                
                # Load and apply stored settings
                await cls._apply_stored_settings()
            else:
                logger.info("[Scheduler] Scheduler already running")
                # Reload settings even if scheduler is already running
                await cls._apply_stored_settings()
        except Exception as e:
            logger.error(f"❌ [Scheduler] Failed to start scheduler: {str(e)}", exc_info=True)
            raise
    
    @classmethod
    async def stop_scheduler(cls):
        """Stop the scheduler"""
        try:
            scheduler = cls.get_scheduler()
            if scheduler.running:
                scheduler.shutdown()
                logger.info("✅ [Scheduler] Scheduler stopped")
        except Exception as e:
            logger.error(f"❌ [Scheduler] Failed to stop scheduler: {str(e)}", exc_info=True)
    
    @classmethod
    async def _apply_stored_settings(cls):
        """Load settings from database and apply them"""
        try:
            async with AsyncSessionLocal() as db:
                result = await db.execute(select(SiteSettings).where(SiteSettings.id == 1))
                settings = result.scalars().first()
                
                if settings:
                    logger.info(f"[Scheduler] Found site settings: enabled={settings.scraping_enabled}, frequency={settings.scraping_frequency}")
                    
                    if settings.scraping_enabled:
                        logger.info("[Scheduler] ✅ Scraping is ENABLED - scheduling job...")
                        await cls._schedule_scraping(db, settings)
                        
                        # Log the final status
                        scheduler = cls.get_scheduler()
                        job = scheduler.get_job(cls._job_id)
                        if job:
                            logger.info(f"[Scheduler] ✅ Job scheduled successfully!")
                            logger.info(f"[Scheduler]   Next run: {job.next_run_time}")
                            logger.info(f"[Scheduler]   Trigger: {job.trigger}")
                        else:
                            logger.warning("[Scheduler] ⚠️ Job was not found after scheduling attempt")
                    else:
                        logger.info("[Scheduler] ⚠️ Scraping is DISABLED - no job scheduled")
                        await cls.stop_scraping_job()
                else:
                    logger.warning("[Scheduler] ⚠️ No site settings found in database - creating defaults...")
                    # Settings will be auto-created with defaults when needed
        except Exception as e:
            logger.error(f"[Scheduler] ❌ Error applying stored settings: {str(e)}", exc_info=True)
    
    @classmethod
    async def update_settings(cls, db: AsyncSession, settings: "SiteSettings"):
        """
        Update scheduler based on new site settings.
        Called when admin updates settings via API.
        """
        try:
            if settings.scraping_enabled:
                logger.info("[Scheduler] Scraping ENABLED - scheduling job")
                await cls._schedule_scraping(db, settings)
            else:
                logger.info("[Scheduler] Scraping DISABLED - removing scheduled job")
                await cls.stop_scraping_job()
        except Exception as e:
            logger.error(f"❌ [Scheduler] Error updating settings: {str(e)}", exc_info=True)
            raise
    
    @classmethod
    async def _schedule_scraping(cls, db: AsyncSession, settings: "SiteSettings"):
        """
        Schedule the scraping job based on frequency settings.
        Replaces any existing job.
        """
        try:
            scheduler = cls.get_scheduler()
            
            # Remove existing job if any
            if scheduler.get_job(cls._job_id):
                scheduler.remove_job(cls._job_id)
                logger.info("[Scheduler] Removed previous scraping job")
            
            # Create trigger based on frequency
            # Get timezone (default to UTC if not specified)
            tz = pytz.timezone(settings.client_timezone) if settings.client_timezone else pytz.UTC
            
            if settings.scraping_frequency == "daily":
                # Parse time (required for daily)
                if not settings.scraping_time:
                    logger.error("[Scheduler] scraping_time is required for daily frequency")
                    return
                time_parts = settings.scraping_time.split(':')
                hour, minute = int(time_parts[0]), int(time_parts[1])
                trigger = CronTrigger(hour=hour, minute=minute, timezone=tz)
                logger.info(f"[Scheduler] Scheduled DAILY scraping at {settings.scraping_time} {settings.client_timezone}")
                
            elif settings.scraping_frequency == "weekly":
                # Parse time (required for weekly)
                if not settings.scraping_time:
                    logger.error("[Scheduler] scraping_time is required for weekly frequency")
                    return
                if not settings.scraping_day_of_week:
                    logger.error("[Scheduler] scraping_day_of_week is required for weekly frequency")
                    return
                time_parts = settings.scraping_time.split(':')
                hour, minute = int(time_parts[0]), int(time_parts[1])
                day_of_week = settings.scraping_day_of_week.lower()
                # Convert day name to number (0=mon, 1=tue, ..., 6=sun)
                day_map = {
                    'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3,
                    'friday': 4, 'saturday': 5, 'sunday': 6
                }
                day_num = day_map.get(day_of_week, 0)
                trigger = CronTrigger(day_of_week=day_num, hour=hour, minute=minute, timezone=tz)
                logger.info(f"[Scheduler] Scheduled WEEKLY scraping every {day_of_week.title()} at {settings.scraping_time} {settings.client_timezone}")
                
            elif settings.scraping_frequency == "custom":
                # For custom, only cron expression is needed (time fields can be null)
                if not settings.scraping_custom_cron:
                    logger.error("[Scheduler] scraping_custom_cron is required for custom frequency")
                    return
                trigger = CronTrigger.from_crontab(settings.scraping_custom_cron, timezone=tz)
                logger.info(f"[Scheduler] Scheduled CUSTOM scraping with cron: {settings.scraping_custom_cron} (timezone: {settings.client_timezone})")
                
            else:
                logger.error(f"[Scheduler] Invalid frequency: {settings.scraping_frequency}")
                return
            
            # Schedule the job
            job = scheduler.add_job(
                cls._scraping_job,
                trigger=trigger,
                id=cls._job_id,
                name="Scheduled Product Scraping",
                args=[settings.enable_discovery, settings.headless_mode, settings.timeout_seconds],
                replace_existing=True,
                coalesce=True,  # Only run once if scheduler was down
                max_instances=1,  # Prevent concurrent runs
            )
            
            logger.info(f"✅ [Scheduler] Scraping job scheduled successfully")
            logger.info(f"[Scheduler] Configuration: Frequency={settings.scraping_frequency}, Discovery={settings.enable_discovery}")
            
        except Exception as e:
            logger.error(f"❌ [Scheduler] Error scheduling scraping: {str(e)}", exc_info=True)
            raise
    
    @classmethod
    async def stop_scraping_job(cls):
        """
        Remove scheduled scraping job.
        Called when scraping is disabled.
        """
        try:
            scheduler = cls.get_scheduler()
            if scheduler.get_job(cls._job_id):
                scheduler.remove_job(cls._job_id)
                logger.info("✅ [Scheduler] Scraping job removed")
            else:
                logger.info("[Scheduler] No active scraping job to remove")
        except Exception as e:
            logger.error(f"❌ [Scheduler] Error stopping scraping job: {str(e)}", exc_info=True)
    
    @classmethod
    def get_scheduler_status(cls) -> Dict[str, Any]:
        """Get detailed scheduler status for debugging"""
        try:
            scheduler = cls.get_scheduler()
            jobs = scheduler.get_jobs()
            job = scheduler.get_job(cls._job_id)
            
            return {
                "running": scheduler.running,
                "type": "persistent" if hasattr(scheduler, '_jobstores') else "in-memory",
                "total_jobs": len(jobs),
                "scraping_job_exists": job is not None,
                "scraping_job": {
                    "id": job.id,
                    "name": job.name,
                    "next_run_time": str(job.next_run_time) if job else None,
                    "trigger": str(job.trigger) if job else None
                } if job else None
            }
        except Exception as e:
            logger.error(f"[Scheduler] Error getting status: {e}")
            return {"error": str(e)}
    
    @staticmethod
    async def _scraping_job(enable_discovery: bool, headless_mode: bool, timeout_seconds: int):
        """
        Actual scheduled scraping job function.
        Scrapes all active products.
        """
        logger.info("\n" + "="*80)
        logger.info("🔔 [Scheduled Scraping] SCHEDULED SCRAPING JOB STARTED")
        logger.info("="*80 + "\n")
        
        start_time = datetime.utcnow()
        total_products = 0
        total_violations = 0
        failed_products = []
        
        try:
            async with AsyncSessionLocal() as db:
                # Fetch all active products
                result = await db.execute(select(Product).where(Product.status == True))
                products = result.scalars().all()
                
                total_products = len(products)
                logger.info(f"[Scheduled Scraping] Found {total_products} active products to scrape")
                
                if total_products == 0:
                    logger.warning("[Scheduled Scraping] No active products to scrape")
                    status = "completed_no_products"
                else:
                    # Scrape each product using UNIFIED approach (Google Search + Shopping Light)
                    for idx, product in enumerate(products, 1):
                        try:
                            logger.info(f"\n[Scheduled Scraping] [{idx}/{total_products}] UNIFIED Scraping: {product.product_name}")
                            logger.info(f"[Scheduled Scraping] Using UNIFIED approach: Google Search + Shopping Light")
                            
                            product_results = []
                            product_violations_list = []
                            search_summary = {
                                "google_search": {"results": 0, "violations": 0, "status": "pending"},
                                "shopping_light": {"results": 0, "violations": 0, "status": "pending"}
                            }
                            
                            # STEP 1: Google Search
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}] [STEP 1/2] 🔍 Google Search...")
                            try:
                                google_results, google_violations = await ScraperService.search_google_serp(
                                    product_name=product.product_name,
                                    barcode=product.barcode,
                                    msp=product.msp,
                                    product=product,
                                    db=db
                                )
                                product_results.extend(google_results)
                                product_violations_list.extend(google_violations)
                                
                                search_summary["google_search"]["results"] = len(google_results)
                                search_summary["google_search"]["violations"] = len(google_violations)
                                search_summary["google_search"]["status"] = "completed"
                                
                                logger.info(f"[Scheduled Scraping] [{idx}/{total_products}] ✅ Google Search: {len(google_results)} results, {len(google_violations)} violations")
                            except Exception as google_err:
                                search_summary["google_search"]["status"] = f"failed - {str(google_err)}"
                                logger.error(f"[Scheduled Scraping] [{idx}/{total_products}] ⚠️ Google Search failed: {str(google_err)}", exc_info=False)
                            
                            # STEP 2: Google Shopping Light
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}] [STEP 2/2] 🛒 Google Shopping Light...")
                            try:
                                shopping_result = await ScraperService.scrape_product_serp(
                                    db,
                                    product_id=product.id,
                                    enable_discovery=enable_discovery,
                                    headless_mode=headless_mode,
                                    timeout_seconds=timeout_seconds
                                )
                                
                                shopping_results = shopping_result.get("results", [])
                                shopping_violations = shopping_result.get("violations", [])
                                
                                product_results.extend(shopping_results)
                                product_violations_list.extend(shopping_violations)
                                
                                search_summary["shopping_light"]["results"] = len(shopping_results)
                                search_summary["shopping_light"]["violations"] = len(shopping_violations)
                                search_summary["shopping_light"]["status"] = "completed"
                                
                                logger.info(f"[Scheduled Scraping] [{idx}/{total_products}] ✅ Shopping Light: {len(shopping_results)} results, {len(shopping_violations)} violations")
                            except Exception as shopping_err:
                                search_summary["shopping_light"]["status"] = f"failed - {str(shopping_err)}"
                                logger.error(f"[Scheduled Scraping] [{idx}/{total_products}] ⚠️ Shopping Light failed: {str(shopping_err)}", exc_info=False)
                            
                            # STEP 3: Deduplication
                            seen_violations = set()
                            deduped_violations = []
                            for v in product_violations_list:
                                violation_key = (
                                    v.get("vendor_name"),
                                    v.get("product_name"),
                                    v.get("msp"),
                                    round(v.get("scraped_price", 0), 2)
                                )
                                if violation_key not in seen_violations:
                                    seen_violations.add(violation_key)
                                    deduped_violations.append(v)
                            
                            product_violations = len(deduped_violations)
                            total_violations += product_violations
                            
                            # Update product with execution timestamp
                            product.last_execution_time = datetime.utcnow().isoformat()
                            await db.commit()
                            
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}] ✅ UNIFIED Completed: {product.product_name}")
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}]   • Total Results: {len(product_results)}")
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}]   • Total Violations (deduplicated): {product_violations}")
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}]   • Google Search: {search_summary['google_search']['status']} ({search_summary['google_search']['results']} results, {search_summary['google_search']['violations']} violations)")
                            logger.info(f"[Scheduled Scraping] [{idx}/{total_products}]   • Shopping Light: {search_summary['shopping_light']['status']} ({search_summary['shopping_light']['results']} results, {search_summary['shopping_light']['violations']} violations)")
                            
                        except Exception as e:
                            logger.error(f"[Scheduled Scraping] [{idx}/{total_products}] ❌ Failed to scrape {product.product_name}: {str(e)}")
                            failed_products.append({
                                'product_id': product.id,
                                'product_name': product.product_name,
                                'error': str(e)
                            })
                    
                    status = "completed_with_errors" if failed_products else "success"
                
                # Update site settings with last run info
                end_time = datetime.utcnow()
                duration = (end_time - start_time).total_seconds()
                
                summary = f"Products: {total_products}, Violations: {total_violations}, Duration: {duration:.1f}s"
                if failed_products:
                    summary += f", Failed: {len(failed_products)}"
                
                await db.execute(
                    update(SiteSettings).where(SiteSettings.id == 1).values(
                        last_scheduled_run=end_time,
                        last_scheduled_run_status=status,
                        last_scheduled_run_summary=summary
                    )
                )
                await db.commit()
                
                logger.info("\n" + "="*80)
                logger.info(f"✅ [Scheduled Scraping] JOB COMPLETED SUCCESSFULLY")
                logger.info(f"[Scheduled Scraping] Total Products: {total_products}")
                logger.info(f"[Scheduled Scraping] Total Violations: {total_violations}")
                logger.info(f"[Scheduled Scraping] Failed Products: {len(failed_products)}")
                logger.info(f"[Scheduled Scraping] Duration: {duration:.1f}s")
                logger.info(f"[Scheduled Scraping] Next Run: Will be executed per schedule configuration")
                logger.info("="*80 + "\n")
                
        except Exception as e:
            logger.error(f"\n❌ [Scheduled Scraping] CRITICAL ERROR: {str(e)}", exc_info=True)
            
            # Update error status
            try:
                async with AsyncSessionLocal() as db:
                    await db.execute(
                        update(SiteSettings).where(SiteSettings.id == 1).values(
                            last_scheduled_run=datetime.utcnow(),
                            last_scheduled_run_status="failed",
                            last_scheduled_run_summary=f"Error: {str(e)}"
                        )
                    )
                    await db.commit()
            except Exception as db_error:
                logger.error(f"[Scheduled Scraping] Could not update error status: {str(db_error)}")
            
            logger.info("="*80 + "\n")
