"""
Database module for storing Meta ad insights in TimescaleDB.
Handles time-series data with metadata caching.
"""

import asyncio
import logging
import os
from datetime import datetime, date
from typing import Any, Dict, List, Optional

import asyncpg
from dotenv import load_dotenv

# Set up logger
logger = logging.getLogger(__name__)


class TimescaleDBClient:
    """Async client for TimescaleDB operations with metadata caching."""

    def __init__(self, connection_string: Optional[str] = None):
        """
        Initialize the database client.

        Args:
            connection_string: PostgreSQL connection string.
                If not provided, will load from DATABASE_URL env var.
        """
        load_dotenv()

        self.connection_string = connection_string or os.getenv("DATABASE_URL")
        if not self.connection_string:
            raise ValueError(
                "Database connection string required. "
                "Set DATABASE_URL in .env or pass connection_string parameter."
            )

        self.pool: Optional[asyncpg.Pool] = None

    async def connect(self):
        """Create connection pool."""
        if not self.pool:
            self.pool = await asyncpg.create_pool(
                self.connection_string,
                min_size=2,
                max_size=10,
                command_timeout=60,
            )
            print("Connected to TimescaleDB")

    async def close(self):
        """Close connection pool."""
        if self.pool:
            await self.pool.close()
            self.pool = None
            print("Disconnected from TimescaleDB")

    async def __aenter__(self):
        """Context manager entry."""
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        await self.close()

    async def initialize_schema(self, schema_path: Optional[str] = None):
        """
        Initialize database schema from SQL file.

        Args:
            schema_path: Path to SQL schema file. If not provided, uses default location.
        """
        if not schema_path:
            # Default to schema file in same directory
            import pathlib
            schema_path = pathlib.Path(__file__).parent / "db_schema.sql"

        print(f"Initializing database schema from {schema_path}...")

        # Read schema file
        with open(schema_path, 'r') as f:
            schema_sql = f.read()

        # Execute schema statement by statement for better error handling
        async with self.pool.acquire() as conn:
            # Set a reasonable timeout for schema operations (5 minutes per statement)
            await conn.execute("SET statement_timeout = '300s'")

            # Split statements and track their line numbers in the original file
            statements = []
            current_line = 1
            for stmt in schema_sql.split(';'):
                stmt_stripped = stmt.strip()
                if stmt_stripped:
                    # Count newlines before this statement to get starting line
                    statements.append((stmt_stripped, current_line))
                # Update line counter (count newlines in the original chunk including ';')
                current_line += stmt.count('\n') + 1

            errors = []
            compression_warnings = []

            for i, (stmt, line_num) in enumerate(statements, 1):
                if not stmt:
                    continue

                # Show progress for potentially slow operations
                stmt_lower = stmt.lower()
                if any(keyword in stmt_lower for keyword in ['refresh materialized view', 'drop schema', 'create index']):
                    print(f"  Executing statement {i} (line {line_num})...")

                try:
                    await conn.execute(stmt)
                except asyncio.TimeoutError:
                    errors.append((i, line_num, "Statement execution timed out (>5 minutes)"))
                except Exception as stmt_error:
                    error_msg = str(stmt_error).lower()

                    # Categorize errors
                    if "already exists" in error_msg:
                        # Silently ignore "already exists" - this is expected on re-runs
                        continue
                    elif "columnstore not enabled" in error_msg:
                        # Track compression warnings separately
                        compression_warnings.append((i, line_num))
                    elif "'nonetype' object has no attribute 'decode'" in error_msg:
                        # Silently ignore decode errors (usually comments/extensions)
                        continue
                    elif "canceling statement due to statement timeout" in error_msg:
                        # Handle PostgreSQL timeout errors
                        errors.append((i, line_num, "Statement execution timed out (>5 minutes)"))
                    else:
                        # Real errors
                        errors.append((i, line_num, stmt_error))

            # Report results
            if errors:
                print(f"⚠️  {len(errors)} error(s) during schema initialization:")
                for stmt_num, line_num, error in errors:
                    print(f"  Statement {stmt_num} (line {line_num}): {error}")

            if compression_warnings:
                print("ℹ️  Note: Data compression not available (TimescaleDB columnstore not enabled)")
                print("   This is optional - your database will work fine without it.")

            print("✓ Database schema initialized successfully")

    # ========================================================================
    # METADATA CACHING (Ad Accounts, Campaigns, Ad Sets)
    # ========================================================================

    async def upsert_ad_account(
        self,
        account_id: str,
        account_name: Optional[str] = None,
        currency: Optional[str] = None,
        timezone_name: Optional[str] = None,
    ):
        """
        Insert or update ad account metadata.

        Args:
            account_id: Meta ad account ID
            account_name: Account name
            currency: Account currency
            timezone_name: Account timezone
        """
        query = """
            INSERT INTO ad_accounts (account_id, account_name, currency, timezone_name, updated_at)
            VALUES ($1, $2, $3, $4, NOW())
            ON CONFLICT (account_id)
            DO UPDATE SET
                account_name = COALESCE(EXCLUDED.account_name, ad_accounts.account_name),
                currency = COALESCE(EXCLUDED.currency, ad_accounts.currency),
                timezone_name = COALESCE(EXCLUDED.timezone_name, ad_accounts.timezone_name),
                updated_at = NOW()
        """
        async with self.pool.acquire() as conn:
            await conn.execute(query, account_id, account_name, currency, timezone_name)

    async def upsert_campaign(
        self,
        campaign_id: str,
        account_id: str,
        campaign_name: str,
        status: Optional[str] = None,
        objective: Optional[str] = None,
    ):
        """
        Insert or update campaign metadata.

        Args:
            campaign_id: Meta campaign ID
            account_id: Parent ad account ID
            campaign_name: Campaign name
            status: Campaign status
            objective: Campaign objective
        """

        query = """
            INSERT INTO campaigns (campaign_id, account_id, campaign_name, status, objective, updated_at)
            VALUES ($1, $2, $3, $4, $5, NOW())
            ON CONFLICT (campaign_id)
            DO UPDATE SET
                campaign_name = CASE
                    WHEN EXCLUDED.campaign_name = 'Unknown' THEN campaigns.campaign_name
                    ELSE EXCLUDED.campaign_name
                END,
                status = COALESCE(EXCLUDED.status, campaigns.status),
                objective = COALESCE(EXCLUDED.objective, campaigns.objective),
                updated_at = NOW()
        """

        logger.debug(f"Executing query with params: {[campaign_id, account_id, campaign_name, status, objective]}")

        async with self.pool.acquire() as conn:
            result = await conn.execute(query, campaign_id, account_id, campaign_name, status, objective)
            logger.debug(f"Query result: {result}")


    async def upsert_adset(
        self,
        adset_id: str,
        campaign_id: str,
        adset_name: str,
        status: Optional[str] = None,
    ):
        """
        Insert or update ad set metadata.

        Args:
            adset_id: Meta ad set ID
            campaign_id: Parent campaign ID
            adset_name: Ad set name
            status: Ad set status
        """
        query = """
            INSERT INTO adsets (adset_id, campaign_id, adset_name, status, updated_at)
            VALUES ($1, $2, $3, $4, NOW())
            ON CONFLICT (adset_id)
            DO UPDATE SET
                adset_name = EXCLUDED.adset_name,
                status = COALESCE(EXCLUDED.status, adsets.status),
                updated_at = NOW()
        """
        async with self.pool.acquire() as conn:
            await conn.execute(query, adset_id, campaign_id, adset_name, status)

    # ========================================================================
    # TIME-SERIES DATA INSERTION
    # ========================================================================

    async def insert_account_insights(
        self,
        time: datetime,
        account_id: str,
        data: Dict[str, Any],
        date_preset: str = "today",
    ):
        """
        Insert account-level insights data.

        Args:
            time: Timestamp for the data point
            account_id: Ad account ID
            data: Insights data dictionary from Meta API
            date_preset: Date preset used (e.g., 'today', 'yesterday')
        """
        query = """
            INSERT INTO account_insights (
                time, account_id, impressions, clicks, spend, reach, frequency,
                ctr, cpc, cpm, cpp, actions, cost_per_action_type, date_preset,
                date_start, date_stop, fetched_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW())
            ON CONFLICT (time, account_id)
            DO UPDATE SET
                impressions = EXCLUDED.impressions,
                clicks = EXCLUDED.clicks,
                spend = EXCLUDED.spend,
                reach = EXCLUDED.reach,
                frequency = EXCLUDED.frequency,
                ctr = EXCLUDED.ctr,
                cpc = EXCLUDED.cpc,
                cpm = EXCLUDED.cpm,
                cpp = EXCLUDED.cpp,
                actions = EXCLUDED.actions,
                cost_per_action_type = EXCLUDED.cost_per_action_type,
                date_preset = EXCLUDED.date_preset,
                date_start = EXCLUDED.date_start,
                date_stop = EXCLUDED.date_stop,
                fetched_at = NOW()
        """

        # Extract metrics with safe defaults
        impressions = int(data.get("impressions", 0)) if data.get("impressions") else None
        clicks = int(data.get("clicks", 0)) if data.get("clicks") else None
        spend = float(data.get("spend", 0)) if data.get("spend") else None
        reach = int(data.get("reach", 0)) if data.get("reach") else None
        frequency = float(data.get("frequency", 0)) if data.get("frequency") else None
        ctr = float(data.get("ctr", 0)) if data.get("ctr") else None
        cpc = float(data.get("cpc", 0)) if data.get("cpc") else None
        cpm = float(data.get("cpm", 0)) if data.get("cpm") else None
        cpp = float(data.get("cpp", 0)) if data.get("cpp") else None

        # Extract date range from Meta API response and convert to date objects
        from datetime import date as Date
        date_start = None
        date_stop = None
        if data.get("date_start"):
            date_start = Date.fromisoformat(data["date_start"])  # "2025-10-21" -> date object
        if data.get("date_stop"):
            date_stop = Date.fromisoformat(data["date_stop"])

        # Store actions as JSONB
        import json
        actions = json.dumps(data.get("actions", [])) if data.get("actions") else None
        cost_per_action = json.dumps(data.get("cost_per_action_type", [])) if data.get("cost_per_action_type") else None

        async with self.pool.acquire() as conn:
            await conn.execute(
                query,
                time, account_id, impressions, clicks, spend, reach, frequency,
                ctr, cpc, cpm, cpp, actions, cost_per_action, date_preset,
                date_start, date_stop
            )

    async def insert_campaign_insights(
        self,
        time: datetime,
        campaign_id: str,
        account_id: str,
        data: Dict[str, Any],
        date_preset: str = "today",
    ):
        """
        Insert campaign-level insights data.

        Args:
            time: Timestamp for the data point
            campaign_id: Campaign ID
            account_id: Ad account ID
            data: Insights data dictionary from Meta API
            date_preset: Date preset used
        """
        # Auto-cache campaign metadata if available in the insights data
        if data.get("campaign_name"):
            await self.upsert_campaign(
                campaign_id=campaign_id,
                account_id=account_id,
                campaign_name=data["campaign_name"],
                status=None,  # Not available in insights response
                objective=None,  # Not available in insights response
            )

        query = """
            INSERT INTO campaign_insights (
                time, campaign_id, account_id, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop, fetched_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW())
            ON CONFLICT (time, campaign_id)
            DO UPDATE SET
                impressions = EXCLUDED.impressions,
                clicks = EXCLUDED.clicks,
                spend = EXCLUDED.spend,
                reach = EXCLUDED.reach,
                ctr = EXCLUDED.ctr,
                cpc = EXCLUDED.cpc,
                cpm = EXCLUDED.cpm,
                actions = EXCLUDED.actions,
                date_preset = EXCLUDED.date_preset,
                date_start = EXCLUDED.date_start,
                date_stop = EXCLUDED.date_stop,
                fetched_at = NOW()
        """

        impressions = int(data.get("impressions", 0)) if data.get("impressions") else None
        clicks = int(data.get("clicks", 0)) if data.get("clicks") else None
        spend = float(data.get("spend", 0)) if data.get("spend") else None
        reach = int(data.get("reach", 0)) if data.get("reach") else None
        ctr = float(data.get("ctr", 0)) if data.get("ctr") else None
        cpc = float(data.get("cpc", 0)) if data.get("cpc") else None
        cpm = float(data.get("cpm", 0)) if data.get("cpm") else None

        # Extract date range from Meta API response and convert to date objects
        from datetime import date as Date
        date_start = None
        date_stop = None
        if data.get("date_start"):
            date_start = Date.fromisoformat(data["date_start"])
        if data.get("date_stop"):
            date_stop = Date.fromisoformat(data["date_stop"])

        import json
        actions = json.dumps(data.get("actions", [])) if data.get("actions") else None

        async with self.pool.acquire() as conn:
            await conn.execute(
                query,
                time, campaign_id, account_id, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop
            )

    async def insert_adset_insights(
        self,
        time: datetime,
        adset_id: str,
        campaign_id: str,
        account_id: str,
        data: Dict[str, Any],
        date_preset: str = "today",
    ):
        """
        Insert ad set level insights data.

        Args:
            time: Timestamp for the data point
            adset_id: Ad set ID
            campaign_id: Campaign ID
            account_id: Ad account ID
            data: Insights data dictionary from Meta API
            date_preset: Date preset used
        """
        # Auto-cache adset metadata if available in the insights data
        # Note: Campaign should already exist from cache_campaigns_metadata or grab_campaign_insights
        # If it doesn't exist, the foreign key constraint will fail with a clear error
        # This is intentional - we should never silently create campaigns with 'Unknown' names
        if data.get("adset_name"):
            await self.upsert_adset(
                adset_id=adset_id,
                campaign_id=campaign_id,
                adset_name=data["adset_name"],
                status=None,  # Not available in insights response
            )

        query = """
            INSERT INTO adset_insights (
                time, adset_id, campaign_id, account_id, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop, fetched_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW())
            ON CONFLICT (time, adset_id)
            DO UPDATE SET
                impressions = EXCLUDED.impressions,
                clicks = EXCLUDED.clicks,
                spend = EXCLUDED.spend,
                reach = EXCLUDED.reach,
                ctr = EXCLUDED.ctr,
                cpc = EXCLUDED.cpc,
                cpm = EXCLUDED.cpm,
                actions = EXCLUDED.actions,
                date_preset = EXCLUDED.date_preset,
                date_start = EXCLUDED.date_start,
                date_stop = EXCLUDED.date_stop,
                fetched_at = NOW()
        """

        impressions = int(data.get("impressions", 0)) if data.get("impressions") else None
        clicks = int(data.get("clicks", 0)) if data.get("clicks") else None
        spend = float(data.get("spend", 0)) if data.get("spend") else None
        reach = int(data.get("reach", 0)) if data.get("reach") else None
        ctr = float(data.get("ctr", 0)) if data.get("ctr") else None
        cpc = float(data.get("cpc", 0)) if data.get("cpc") else None
        cpm = float(data.get("cpm", 0)) if data.get("cpm") else None

        # Extract date range from Meta API response and convert to date objects
        from datetime import date as Date
        date_start = None
        date_stop = None
        if data.get("date_start"):
            date_start = Date.fromisoformat(data["date_start"])
        if data.get("date_stop"):
            date_stop = Date.fromisoformat(data["date_stop"])

        import json
        actions = json.dumps(data.get("actions", [])) if data.get("actions") else None

        async with self.pool.acquire() as conn:
            await conn.execute(
                query,
                time, adset_id, campaign_id, account_id, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop
            )

    async def insert_campaign_insights_by_country(
        self,
        time: datetime,
        campaign_id: str,
        account_id: str,
        country: str,
        data: Dict[str, Any],
        date_preset: str = "today",
    ):
        """
        Insert campaign-level insights data broken down by country.

        Args:
            time: Timestamp for the data point
            campaign_id: Campaign ID
            account_id: Ad account ID
            country: ISO 2-letter country code
            data: Insights data dictionary from Meta API
            date_preset: Date preset used
        """
        query = """
            INSERT INTO campaign_insights_by_country (
                time, campaign_id, account_id, country, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop, fetched_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW())
            ON CONFLICT (time, campaign_id, country)
            DO UPDATE SET
                impressions = EXCLUDED.impressions,
                clicks = EXCLUDED.clicks,
                spend = EXCLUDED.spend,
                reach = EXCLUDED.reach,
                ctr = EXCLUDED.ctr,
                cpc = EXCLUDED.cpc,
                cpm = EXCLUDED.cpm,
                actions = EXCLUDED.actions,
                date_preset = EXCLUDED.date_preset,
                date_start = EXCLUDED.date_start,
                date_stop = EXCLUDED.date_stop,
                fetched_at = NOW()
        """

        impressions = int(data.get("impressions", 0)) if data.get("impressions") else None
        clicks = int(data.get("clicks", 0)) if data.get("clicks") else None
        spend = float(data.get("spend", 0)) if data.get("spend") else None
        reach = int(data.get("reach", 0)) if data.get("reach") else None
        ctr = float(data.get("ctr", 0)) if data.get("ctr") else None
        cpc = float(data.get("cpc", 0)) if data.get("cpc") else None
        cpm = float(data.get("cpm", 0)) if data.get("cpm") else None

        # Extract date range from Meta API response and convert to date objects
        from datetime import date as Date
        date_start = None
        date_stop = None
        if data.get("date_start"):
            date_start = Date.fromisoformat(data["date_start"])
        if data.get("date_stop"):
            date_stop = Date.fromisoformat(data["date_stop"])

        import json
        actions = json.dumps(data.get("actions", [])) if data.get("actions") else None

        async with self.pool.acquire() as conn:
            await conn.execute(
                query,
                time, campaign_id, account_id, country, impressions, clicks, spend, reach,
                ctr, cpc, cpm, actions, date_preset, date_start, date_stop
            )

    # ========================================================================
    # QUERY HELPERS
    # ========================================================================

    async def get_latest_account_metrics(self, account_id: str) -> Optional[Dict[str, Any]]:
        """
        Get the most recent metrics for an account.

        Args:
            account_id: Ad account ID

        Returns:
            Dictionary of latest metrics or None
        """
        query = """
            SELECT * FROM latest_account_metrics
            WHERE account_id = $1
        """
        async with self.pool.acquire() as conn:
            row = await conn.fetchrow(query, account_id)
            return dict(row) if row else None

    async def get_account_metrics_range(
        self,
        account_id: str,
        start_time: datetime,
        end_time: datetime,
    ) -> List[Dict[str, Any]]:
        """
        Get account metrics for a time range.

        Args:
            account_id: Ad account ID
            start_time: Start of time range
            end_time: End of time range

        Returns:
            List of metric dictionaries
        """
        query = """
            SELECT * FROM account_insights
            WHERE account_id = $1 AND time BETWEEN $2 AND $3
            ORDER BY time DESC
        """
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(query, account_id, start_time, end_time)
            return [dict(row) for row in rows]

    async def get_existing_dates_for_account(
        self,
        account_id: str,
        start_date: date,
        end_date: date,
    ) -> List[date]:
        """
        Get all dates that already have data for an account.

        Args:
            account_id: Ad account ID
            start_date: Start date (inclusive)
            end_date: End date (inclusive)

        Returns:
            List of dates that have existing data
        """
        query = """
            SELECT DISTINCT date_start
            FROM account_insights
            WHERE account_id = $1
                AND date_start IS NOT NULL
                AND date_start BETWEEN $2 AND $3
            ORDER BY date_start
        """
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(query, account_id, start_date, end_date)
            return [row['date_start'] for row in rows]
