from datetime import timedelta
import time
import random

from utils.snowflake.utils import get_data_from_snowflake
from psycopg2.extras import execute_values
from utils.postgres.postgres_client import get_postgres_connection
from utils.util import get_environment
from typing import List, Callable, Optional

environment = get_environment()
database_name = f"SUNO_{environment}"


# Insert batch into target table
def insert_batch(batch, write_pg_cursor, write_pg_connection, read_pg_cursor, insert_sql, filter_invalid_rows_func):
    if filter_invalid_rows_func:
        original_count = len(batch)
        filtered_batch = filter_invalid_rows_func(batch, read_pg_cursor)
        batch[:] = filtered_batch
        print(f"Filtered batch from {original_count} to {len(batch)} rows (only clip_ids in bots_generatedclip)")
    else:
        print(f"No filter_invalid_rows_func provided, inserting all {len(batch)} rows")
    
    if not batch:
        print("No valid clip_ids found in batch, skipping insert")
        return
    
    for retry in range(3):
        sleep_time = 0.5 * (2**retry) + random.uniform(0, 0.1)
        time.sleep(sleep_time)
        try:
            execute_values(
                write_pg_cursor,
                insert_sql,
                batch,
            )
            write_pg_connection.commit()
            print(f"Inserted {len(batch)} rows")
            break
        except Exception as e:
            print(f"Error processing batch: {e}")
            continue
            

def backfill_data(spark, data, insert_sql, APPLICATION_NAME, row_mapping_func, filter_invalid_rows_func):    
    try:
        # Process data directly - no DataFrame needed, avoids schema inference issues
        print(f"Processing {len(data):,} rows directly from Snowflake")
        
        write_pg_connection = get_postgres_connection(APPLICATION_NAME)
        write_pg_cursor = write_pg_connection.cursor()
        read_pg_connection = get_postgres_connection(APPLICATION_NAME)
        read_pg_cursor = read_pg_connection.cursor()
        
        batch = []
        batch_size = 1000
        
        for row in data:
            try:
                # Use row mapping function to convert row to tuple
                mapped_row = row_mapping_func(row)
                batch.append(mapped_row)
                
                if len(batch) >= batch_size:
                    insert_batch(batch, write_pg_cursor, write_pg_connection, read_pg_cursor, insert_sql, filter_invalid_rows_func)
                    batch = []

            except Exception as e:
                print(f"Error processing row: {row}")
                raise e

        # Process remaining batch
        if batch:
            insert_batch(batch, write_pg_cursor, write_pg_connection, read_pg_cursor, insert_sql, filter_invalid_rows_func)
        
        # Clean up connections
        write_pg_cursor.close()
        write_pg_connection.close()
        read_pg_cursor.close()
        read_pg_connection.close()

        return len(data)
    except Exception as e:
        print(f"Error processing data: {str(e)}")
        raise e


# Backfill data day by day
def backfill_data_day_by_day_from_snowflake(spark, start_date, end_date, snowflake_query_sql, insert_sql, row_mapping_func, APPLICATION_NAME, filter_invalid_rows_func=None):
    total_processed = 0
    failed_dates = []
    try:
        print(f"Starting backfill from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
        current_date = start_date
        while current_date <= end_date:
            try:
                query_sql = snowflake_query_sql.format(process_date=current_date.strftime('%Y-%m-%d'))
                data = get_data_from_snowflake("SUNO_GLUE_BACKFILL_LARGE", database_name, environment, query_sql)
                processed_count = backfill_data(spark, data, insert_sql, APPLICATION_NAME, row_mapping_func, filter_invalid_rows_func)
                total_processed += processed_count  
                print(f"Successfully processed {processed_count} rows for {current_date}")
                if processed_count > 0:
                    time.sleep(1)
            except Exception as e:
                print(f"Failed to process {current_date.strftime('%Y-%m-%d')}: {str(e)}")
                failed_dates.append(current_date.strftime('%Y-%m-%d'))    
            current_date += timedelta(days=1)

        print(f"Total rows processed: {total_processed:,}")
        if failed_dates:
            print(f"Failed dates ({len(failed_dates)}): {', '.join(failed_dates)}")
            raise Exception(f"Failed dates ({len(failed_dates)}): {', '.join(failed_dates)}")
        else:
            print("All dates processed successfully!")
    except Exception as e:
        print(f"Critical error occurred: {str(e)}")
        raise e


# Update batch into target table
def update_batch(batch: List, write_pg_cursor, write_pg_connection, update_sql: str, filter_invalid_rows_func: Optional[Callable] = None):
    """
    Update a batch of records in PostgreSQL using raw SQL method
    
    Args:
        batch: List of data rows to update
        write_pg_cursor: PostgreSQL cursor for writing
        write_pg_connection: PostgreSQL connection for writing
        update_sql: SQL update statement with {format_strings} placeholder
        filter_invalid_rows_func: Optional function to filter rows before update
    """
    if filter_invalid_rows_func:
        read_pg_connection = get_postgres_connection("analystic-database-connection")
        read_pg_cursor = read_pg_connection.cursor()
        try:
            original_count = len(batch)
            batch = filter_invalid_rows_func(batch, read_pg_cursor)
            print(f"Filtered batch from {original_count} to {len(batch)} rows")
        finally:
            read_pg_cursor.close()
            read_pg_connection.close()
    else:
        print(f"No filter_invalid_rows_func provided, updating all {len(batch)} rows")
    
    if not batch:
        print("No valid records found in batch, skipping update")
        return
    
    for retry in range(3):
        sleep_time = 0.5 * (2**retry) + random.uniform(0, 0.1)
        time.sleep(sleep_time)
        try:
            # Create format_strings placeholder for IN clause using raw SQL method
            ids_in_batch = [row[0] for row in batch if row[0] is not None]
            quoted_ids = [f"'{id}'" for id in ids_in_batch]
            format_strings_raw = ",".join(quoted_ids)
            # Only replace the specific {format_strings} parameter, not all braces
            raw_sql = update_sql.format(format_strings=format_strings_raw)
            print(f"Executing raw SQL update with {len(ids_in_batch)} values")
            write_pg_cursor.execute(raw_sql)
            write_pg_connection.commit()
            updated_count = write_pg_cursor.rowcount if hasattr(write_pg_cursor, 'rowcount') else len(batch)
            print(f"Updated {updated_count} rows")
            break
        except Exception as e:
            write_pg_connection.rollback()
            if retry < 2:
                print(f"Update failed (attempt {retry + 1}/3): {str(e)}, retrying...")
            else:
                print(f"Update failed after 3 attempts: {str(e)}")
                raise e


# Update data from Snowflake to PostgreSQL
def update_data(spark, data: List, update_sql: str, APPLICATION_NAME: str, row_mapping_func: Callable, filter_invalid_rows_func: Optional[Callable] = None):
    """
    Update data from Snowflake to PostgreSQL in batches
    
    Args:
        spark: Spark session
        data: List of Snowflake data rows
        update_sql: SQL update statement
        APPLICATION_NAME: Application name for connection
        row_mapping_func: Function to convert Snowflake row to PostgreSQL format
        filter_invalid_rows_func: Optional function to filter invalid rows
        
    Returns:
        int: Number of rows processed
    """
    if not data or len(data) == 0:
        print("No data to update")
        return 0

    try:
        write_pg_connection = get_postgres_connection("analystic-database-connection")
        write_pg_cursor = write_pg_connection.cursor()

        # Set application name for monitoring
        write_pg_cursor.execute(f"SET application_name = '{APPLICATION_NAME}'")
        write_pg_connection.commit()

        batch = []
        batch_size = 1000

        for row in data:
            batch.append(row_mapping_func(row))
            if len(batch) >= batch_size:
                update_batch(batch, write_pg_cursor, write_pg_connection, update_sql, filter_invalid_rows_func)
                batch = []

        # Update remaining batch
        if batch:
            update_batch(batch, write_pg_cursor, write_pg_connection, update_sql, filter_invalid_rows_func)

        write_pg_cursor.close()
        write_pg_connection.close()

        return len(data)
    except Exception as e:
        print(f"Error processing update data: {str(e)}")
        raise e


# Update data day by day from Snowflake
def update_data_day_by_day_from_snowflake(
    spark, 
    start_date, 
    end_date, 
    snowflake_query_sql: str, 
    update_sql: str, 
    row_mapping_func: Callable, 
    APPLICATION_NAME: str, 
    filter_invalid_rows_func: Optional[Callable] = None
):
    """
    Update data day by day from Snowflake to PostgreSQL
    
    Args:
        spark: Spark session
        start_date: Start date for processing
        end_date: End date for processing
        snowflake_query_sql: Snowflake SQL query with {process_date} placeholder
        update_sql: PostgreSQL update SQL statement
        row_mapping_func: Function to convert Snowflake row to PostgreSQL format
        APPLICATION_NAME: Application name for connection monitoring
        filter_invalid_rows_func: Optional function to filter invalid rows
        
    Returns:
        int: Total number of rows processed
    """
    total_processed = 0
    failed_dates = []
    try:
        print(f"Starting day-by-day update from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
        current_date = start_date
        while current_date <= end_date:
            try:
                query_sql = snowflake_query_sql.format(process_date=current_date.strftime('%Y-%m-%d'))
                data = get_data_from_snowflake("SUNO_GLUE_BACKFILL_LARGE", database_name, environment, query_sql)
                processed_count = update_data(spark, data, update_sql, APPLICATION_NAME, row_mapping_func, filter_invalid_rows_func)
                total_processed += processed_count  
                print(f"Successfully processed {processed_count} rows for {current_date.strftime('%Y-%m-%d')}")
                if processed_count > 0:
                    time.sleep(1)
            except Exception as e:
                print(f"Failed to process {current_date.strftime('%Y-%m-%d')}: {str(e)}")
                failed_dates.append(current_date.strftime('%Y-%m-%d'))    
            current_date += timedelta(days=1)

        print(f"Total rows processed: {total_processed:,}")
        if failed_dates:
            print(f"Failed dates ({len(failed_dates)}): {', '.join(failed_dates)}")
            raise Exception(f"Failed dates ({len(failed_dates)}): {', '.join(failed_dates)}")
        else:
            print("All dates processed successfully!")
            
        return total_processed
    except Exception as e:
        print(f"Critical error occurred during day-by-day update: {str(e)}")
        raise e
