"""
View Manager Setup and Daemon

This script:
1. Initializes database schema from db_schema.sql (drops and recreates public schema with all views)
2. Loads metadata from metadata.yaml into the database
3. Auto-detects materialized views from db_schema.sql
4. Periodically refreshes materialized views (configurable interval)

Usage:
    uv run view-manager-setup

Environment variables:
    REFRESH_INTERVAL_MINUTES: How often to refresh materialized views (default: 60)
"""

import asyncio
import csv
import logging
import os
import re
import signal
import sys
from pathlib import Path
from typing import List

import yaml

from meta_api_grabber.database import TimescaleDBClient

# Set up logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


class ViewManagerSetup:
    """Manages database schema setup, metadata loading, and view refresh."""

    def __init__(self, refresh_interval_minutes: int = 60):
        self.db = TimescaleDBClient()
        self.shutdown_event = asyncio.Event()
        self.refresh_interval_minutes = refresh_interval_minutes
        self.materialized_views: List[str] = []

    async def load_metadata_from_yaml(self, yaml_path: Path):
        """
        Load account metadata from YAML file and sync to database.

        Args:
            yaml_path: Path to metadata.yaml file
        """
        if not yaml_path.exists():
            logger.warning(f"Metadata file not found: {yaml_path}")
            logger.info("Skipping metadata loading. Create metadata.yaml to configure accounts.")
            return

        logger.info(f"Loading metadata from {yaml_path}")

        with open(yaml_path, 'r') as f:
            data = yaml.safe_load(f)

        accounts = data.get('accounts', [])
        if not accounts:
            logger.warning("No accounts found in metadata.yaml")
            return

        # Sync accounts to database using upsert logic
        async with self.db.pool.acquire() as conn:
            # Clear existing metadata (or implement smarter sync logic)
            await conn.execute("DELETE FROM public.account_metadata")

            for account in accounts:
                label = account.get('label')
                meta_account_id = account.get('meta_account_id')
                google_account_id = account.get('google_account_id')
                alpinebits_hotel_code = account.get('alpinebits_hotel_code')

                if not label:
                    logger.warning(f"Skipping account without label: {account}")
                    continue

                await conn.execute(
                    """
                    INSERT INTO public.account_metadata
                        (label, meta_account_id, google_account_id, alpinebits_hotel_code, updated_at)
                    VALUES ($1, $2, $3, $4, NOW())
                    """,
                    label, meta_account_id, google_account_id, alpinebits_hotel_code
                )

                logger.info(f"Loaded metadata for: {label}")

        logger.info(f"Successfully loaded {len(accounts)} accounts from metadata.yaml")

    async def load_geotargets_from_csv(self, csv_path: Path):
        """
        Load geotargets data from CSV file and sync to database.

        This function is idempotent - it will:
        1. Clear existing geotargets data
        2. Load fresh data from CSV

        This means if you swap the CSV file, the data will be updated.

        Args:
            csv_path: Path to geotargets CSV file
        """
        if not csv_path.exists():
            logger.warning(f"Geotargets CSV file not found: {csv_path}")
            logger.info("Skipping geotargets loading. Place geotargets CSV file to load location data.")
            return

        logger.info(f"Loading geotargets from {csv_path}")

        # Read CSV file
        geotargets = []
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                # Convert IDs to integers, handling empty parent_id
                criteria_id = int(row['Criteria ID'])
                parent_id = int(row['Parent ID']) if row['Parent ID'] else None

                geotargets.append({
                    'criteria_id': criteria_id,
                    'name': row['Name'],
                    'canonical_name': row['Canonical Name'],
                    'parent_id': parent_id,
                    'country_code': row['Country Code'],
                    'target_type': row['Target Type'],
                    'status': row['Status']
                })

        if not geotargets:
            logger.warning("No geotargets found in CSV file")
            return

        # Load data to database using idempotent approach
        async with self.db.pool.acquire() as conn:
            # Clear existing data to ensure we have latest data from CSV
            deleted_count = await conn.fetchval("SELECT COUNT(*) FROM public.geotargets")
            await conn.execute("TRUNCATE TABLE public.geotargets")
            logger.info(f"Cleared {deleted_count} existing geotarget records")

            # Batch insert new data
            await conn.executemany(
                """
                INSERT INTO public.geotargets
                    (criteria_id, name, canonical_name, parent_id, country_code, target_type, status, updated_at)
                VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
                """,
                [(g['criteria_id'], g['name'], g['canonical_name'], g['parent_id'],
                  g['country_code'], g['target_type'], g['status'])
                 for g in geotargets]
            )

        logger.info(f"Successfully loaded {len(geotargets)} geotargets from CSV")

    def detect_materialized_views(self, schema_path: Path) -> List[str]:
        """
        Auto-detect materialized views from db_schema.sql.

        Parses the SQL file to find:
        1. CREATE MATERIALIZED VIEW statements
        2. REFRESH MATERIALIZED VIEW statements (explicit refresh requests)

        Args:
            schema_path: Path to db_schema.sql

        Returns:
            List of materialized view names (deduplicated, in order of appearance)
        """
        if not schema_path.exists():
            logger.warning(f"Schema file not found: {schema_path}")
            return []

        with open(schema_path, 'r') as f:
            content = f.read()

        views = []

        # Find CREATE MATERIALIZED VIEW statements
        # Matches: CREATE MATERIALIZED VIEW [IF NOT EXISTS] view_name AS
        create_pattern = r'CREATE\s+MATERIALIZED\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+AS'
        create_matches = re.findall(create_pattern, content, re.IGNORECASE)
        views.extend(create_matches)

        # Find REFRESH MATERIALIZED VIEW statements
        # Matches: REFRESH MATERIALIZED VIEW [CONCURRENTLY] view_name
        refresh_pattern = r'REFRESH\s+MATERIALIZED\s+VIEW\s+(?:CONCURRENTLY\s+)?(?:public\.)?(\w+)'
        refresh_matches = re.findall(refresh_pattern, content, re.IGNORECASE)
        views.extend(refresh_matches)

        # Deduplicate while preserving order
        seen = set()
        deduplicated = []
        for view in views:
            if view not in seen:
                seen.add(view)
                deduplicated.append(view)

        if deduplicated:
            logger.info(f"Detected {len(deduplicated)} materialized view(s): {', '.join(deduplicated)}")
        else:
            logger.info("No materialized views detected in db_schema.sql")

        return deduplicated

    async def refresh_materialized_views(self):
        """Refresh all detected materialized views."""
        if not self.materialized_views:
            logger.debug("No materialized views to refresh")
            return

        logger.info(f"Refreshing {len(self.materialized_views)} materialized view(s)...")

        for view_name in self.materialized_views:
            try:
                # Try CONCURRENTLY first (requires unique index)
                await self.db.pool.execute(
                    f"REFRESH MATERIALIZED VIEW CONCURRENTLY public.{view_name}"
                )
                logger.info(f"✓ Refreshed {view_name} (CONCURRENTLY)")

            except Exception as e:
                error_msg = str(e).lower()

                # If CONCURRENTLY fails, try regular refresh
                if 'concurrently' in error_msg or 'unique index' in error_msg:
                    try:
                        await self.db.pool.execute(
                            f"REFRESH MATERIALIZED VIEW public.{view_name}"
                        )
                        logger.info(f"✓ Refreshed {view_name}")
                    except Exception as e2:
                        logger.error(f"✗ Failed to refresh {view_name}: {e2}")
                else:
                    logger.error(f"✗ Failed to refresh {view_name}: {e}")

        logger.info("Materialized view refresh completed")

    async def setup(self):
        """Initialize database schema and load metadata."""
        try:
            # Connect to database
            await self.db.connect()

            # Initialize schema (this will drop and recreate public schema)
            logger.info("Initializing database schema...")
            schema_path = Path(__file__).parent / "db_schema.sql"
            await self.db.initialize_schema()
            logger.info("Database schema initialized successfully")

            # Auto-detect materialized views
            self.materialized_views = self.detect_materialized_views(schema_path)

            # Load metadata from YAML
            metadata_path = Path(__file__).parent.parent.parent / "metadata.yaml"
            await self.load_metadata_from_yaml(metadata_path)

            # Load geotargets from CSV
            # Look for the most recent geotargets CSV file in the package directory
            geotargets_dir = Path(__file__).parent
            geotargets_files = sorted(geotargets_dir.glob("geotargets*.csv"), reverse=True)
            if geotargets_files:
                geotargets_path = geotargets_files[0]
                await self.load_geotargets_from_csv(geotargets_path)
            else:
                logger.warning("No geotargets CSV file found in package directory")

            # Initial refresh of materialized views
            if self.materialized_views:
                logger.info("Performing initial refresh of materialized views...")
                await self.refresh_materialized_views()

            logger.info("Setup completed successfully")

        except Exception as e:
            logger.error(f"Setup failed: {e}", exc_info=True)
            raise

    async def periodic_refresh_loop(self):
        """
        Periodically refresh materialized views.

        Runs in a loop until shutdown is signaled.
        """
        if not self.materialized_views:
            logger.info("No materialized views to refresh, skipping periodic refresh")
            return

        logger.info(f"Starting periodic refresh loop (interval: {self.refresh_interval_minutes} minutes)")

        while not self.shutdown_event.is_set():
            try:
                # Wait for the refresh interval or shutdown signal
                await asyncio.wait_for(
                    self.shutdown_event.wait(),
                    timeout=self.refresh_interval_minutes * 60
                )
                # If we get here, shutdown was signaled
                break

            except asyncio.TimeoutError:
                # Timeout means it's time to refresh
                logger.info("Starting scheduled materialized view refresh...")
                try:
                    await self.refresh_materialized_views()
                except Exception as e:
                    logger.error(f"Error during scheduled refresh: {e}", exc_info=True)
                    # Continue the loop even if refresh fails

    async def wait_for_tasks(self):
        """
        Run periodic tasks and wait for shutdown signal.

        Currently runs:
        - Periodic materialized view refresh (configurable interval)

        In the future, this could also:
        - Poll Airbyte API for completed syncs
        - Serve health check endpoints
        - Listen to webhooks
        """
        logger.info("View manager is ready")
        logger.info("Press Ctrl+C to shutdown")

        # Run periodic refresh loop
        await self.periodic_refresh_loop()

        logger.info("Periodic refresh loop stopped")

    async def run(self):
        """Main entry point."""
        try:
            # Setup database and metadata
            await self.setup()

            # Wait for tasks or shutdown
            await self.wait_for_tasks()

        finally:
            logger.info("Shutting down...")
            await self.db.close()
            logger.info("Shutdown complete")

    def handle_shutdown(self, signum, frame):
        """Handle shutdown signals gracefully."""
        logger.info(f"Received signal {signum}, initiating shutdown...")
        self.shutdown_event.set()


def main():
    """CLI entry point."""
    # Get refresh interval from environment variable
    refresh_interval = int(os.getenv('REFRESH_INTERVAL_MINUTES', '60'))
    logger.info(f"Configured refresh interval: {refresh_interval} minutes")

    manager = ViewManagerSetup(refresh_interval_minutes=refresh_interval)

    # Register signal handlers for graceful shutdown
    signal.signal(signal.SIGINT, manager.handle_shutdown)
    signal.signal(signal.SIGTERM, manager.handle_shutdown)

    # Run the async main function
    try:
        asyncio.run(manager.run())
    except KeyboardInterrupt:
        logger.info("Interrupted by user")
        sys.exit(0)
    except Exception as e:
        logger.error(f"Fatal error: {e}", exc_info=True)
        sys.exit(1)


if __name__ == "__main__":
    main()
