import os
import sys
import json
import uuid
import traceback
import datetime
from typing import List, Optional, Dict, Any, Union
import psycopg
from noetl.core.common import make_serializable, get_snowflake_id
from noetl.core.logger import setup_logger

logger = setup_logger(__name__, include_location=True)

class DatabaseSchema:

    def __init__(self, conn=None, pgdb: str = None, admin_conn: str = None, noetl_user: str = None, noetl_password: str = None, noetl_schema: str = None, auto_setup: bool = False):
        """
        Initialize the Database Schema.

        Args:
            conn: A database connection (will be created if None)
            pgdb: Postgres connection string for noetl user
            admin_conn: Admin connection string for creating user/schema
            noetl_user: Username for noetl operations (will be determined from config if None)
            noetl_password: Password for noetl user (will be determined from config if None)
            noetl_schema: Schema name for noetl tables (will be determined from config if None)
            auto_setup: If True, automatically create user/schema during initialization (default: False)
        """
        self.conn = conn
        self.pgdb = pgdb
        self.admin_conn = admin_conn
        self.auto_setup = auto_setup
        self.is_postgres = False
        self.admin_connection = None

        try:
            from noetl.core.config import get_settings
            self.settings = get_settings()
            self.set_noetl_credentials(noetl_user, noetl_password, noetl_schema)
        except Exception as e:
            logger.error(f"FATAL: Failed to initialize database schema: {e}")
            logger.error("FATAL: Exiting immediately due to configuration error")
            sys.exit(1)

    async def initialize_async(self):
        """Async initialization method that must be called after __init__"""
        try:
            if self.conn is None:
                await self.initialize_connection()
        except Exception as e:
            logger.error(f"FATAL: Failed to initialize async database connection: {e}")
            logger.error("FATAL: Exiting immediately due to database connection failure")
            sys.exit(1)

    def set_noetl_credentials(self, noetl_user: str = None, noetl_password: str = None, noetl_schema: str = None):
        self.noetl_user = noetl_user or self.settings.noetl_user
        self.noetl_password = noetl_password or self.settings.noetl_password
        self.noetl_schema = noetl_schema or self.settings.noetl_schema

        logger.info(f"Using NoETL credentials for setup, user: {self.noetl_user}, schema: {self.noetl_schema}")


    async def initialize_connection(self):
        try:
            if self.admin_conn is None:
                self.admin_conn = self.settings.admin_conn_string
                logger.info(f"Using admin connection: dbname={self.settings.postgres_db} user={self.settings.postgres_user} host={self.settings.postgres_host} port={self.settings.postgres_port}")

            if self.pgdb is None:
                self.pgdb = self.settings.noetl_conn_string
                logger.info(f"NoETL async connection: dbname={self.settings.postgres_db} user={self.noetl_user} host={self.settings.postgres_host} port={self.settings.postgres_port}")

            if self.auto_setup:
                if getattr(self, "settings", None) and getattr(self.settings, "noetl_drop_schema", False):
                    await self.drop_noetl_schema_async()
                await self.create_noetl_schema()

            try:
                self.conn = await psycopg.AsyncConnection.connect(self.pgdb)
                await self.conn.set_autocommit(True)
                self.is_postgres = True
                logger.info("Connected to Postgres database as noetl user (async).")
            except psycopg.OperationalError as e:
                logger.error(f"NoETL user async connection failed, attempting to create user/schema: {e}")
                await self.create_noetl_schema()
                try:
                    self.conn = await psycopg.AsyncConnection.connect(self.pgdb)
                    await self.conn.set_autocommit(True)
                    self.is_postgres = True
                    logger.info("Connected to Postgres database as noetl user after creating infrastructure (async).")
                except Exception as conn_error:
                    logger.exception(f"FATAL: Failed to connect after schema creation: {conn_error}")
                    raise

        except Exception as e:
            logger.exception(f"FATAL: Failed to establish async connection to Postgres: {e}")
            raise


    def initialize_connection_sync(self):
        try:
            if self.admin_conn is None:
                self.admin_conn = self.settings.admin_conn_string
                logger.info(f"Using admin connection: dbname={self.settings.postgres_db} user={self.settings.postgres_user} host={self.settings.postgres_host} port={self.settings.postgres_port}")

            if self.pgdb is None:
                self.pgdb = self.settings.noetl_conn_string
                logger.info(f"NoETL sync connection: dbname={self.settings.postgres_db} user={self.noetl_user} host={self.settings.postgres_host} port={self.settings.postgres_port}")

            if self.auto_setup and (self.conn is None):
                try:
                    self.conn = psycopg.connect(self.pgdb)
                    self.conn.autocommit = True
                    self.is_postgres = True
                    logger.info("Connected to Postgres database as noetl user (sync).")
                except Exception as e:
                    logger.warning(f"NoETL user sync connection failed, attempting to create user/schema: {e}")
                    try:
                        admin = psycopg.connect(self.admin_conn)
                        try:
                            admin.autocommit = True
                            with admin.cursor() as ac:
                                ac.execute("""
                                    DO $$
                                    BEGIN
                                        IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN
                                            EXECUTE format('CREATE USER %s WITH PASSWORD %L CREATEDB LOGIN', %s, %s, %s);
                                        END IF;
                                    END $$;
                                """, (self.noetl_user, self.noetl_user, self.noetl_password, self.noetl_user))
                                ac.execute("""
                                    DO $$
                                    BEGIN
                                        IF NOT EXISTS (
                                            SELECT 1 FROM information_schema.schemata WHERE schema_name = %s
                                        ) THEN
                                            EXECUTE format('CREATE SCHEMA %I', %s);
                                        END IF;
                                    END $$;
                                """, (self.noetl_schema, self.noetl_schema))
                                try:
                                    ac.execute(f"GRANT ALL PRIVILEGES ON SCHEMA {self.noetl_schema} TO {self.noetl_user}")
                                except Exception as e:
                                    logger.error(f"Failed to grant schema privileges to user {self.noetl_user}: {e}")
                                    logger.exception("Schema privilege grant failed:")
                                    # Continue but log the issue - this could cause permission problems later
                        finally:
                            admin.close()
                        self.conn = psycopg.connect(self.pgdb)
                        self.conn.autocommit = True
                        self.is_postgres = True
                        logger.info("Connected to Postgres database as noetl user after creating infrastructure (sync).")
                    except Exception as conn_error:
                        logger.error(f"FATAL: Failed to connect after schema creation (sync): {conn_error}")
                        raise
            else:
                if self.conn is None:
                    self.conn = psycopg.connect(self.pgdb)
                    self.conn.autocommit = True
                    self.is_postgres = True
                    logger.info("Connected to Postgres database as noetl user (sync).")
        except Exception as e:
            logger.error(f"FATAL: Failed to establish sync connection to Postgres: {e}")
            raise

    def test_connection_sync(self):
        try:
            if self.conn is None:
                logger.error("FATAL: Database connection is None. Connection was not established properly (sync).")
                raise ValueError("Database connection is None")
            with self.conn.cursor() as cursor:
                cursor.execute("SELECT 1")
                result = cursor.fetchone()
                if not (result and result[0] == 1):
                    raise ValueError("Database connection test failed (sync)")
                logger.info("Database connected (sync).")
        except Exception as e:
            logger.error(f"FATAL: Error testing database connection (sync): {e}.", exc_info=True)
            raise

    def set_search_path_sync(self):
        try:
            with self.conn.cursor() as cursor:
                cursor.execute(f"SET search_path TO {self.noetl_schema}, public")
                if not getattr(self.conn, "autocommit", False):
                    self.conn.commit()
                logger.info(f"Search path set to '{self.noetl_schema}, public' (sync)")
        except Exception as e:
            logger.error(f"Error setting search path (sync): {e}.", exc_info=True)
            raise

    def create_postgres_tables_sync(self):
        try:
            # Execute canonical SQL DDL from file and return
            ddl_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "database", "ddl", "postgres", "schema_ddl.sql"))
            if not os.path.isfile(ddl_path):
                raise FileNotFoundError(f"Canonical DDL file not found at {ddl_path}")
            with self.conn.cursor() as cursor:
                with open(ddl_path, "r", encoding="utf-8") as f:
                    ddl_sql = f.read()
                cursor.execute(ddl_sql)
                if not getattr(self.conn, "autocommit", False):
                    self.conn.commit()
                logger.info("Executed canonical Postgres DDL from schema_ddl.sql (sync).")
            return
        except Exception as e:
            logger.error(f"FATAL: Error creating postgres tables from canonical DDL (sync): {e}", exc_info=True)
            raise

    def init_database_sync(self):
        try:
            self.test_connection_sync()
            self.set_search_path_sync()
            self.create_postgres_tables_sync()
            return True
        except Exception as e:
            logger.error(f"FATAL: Error initializing database (sync): {e}.", exc_info=True)
            raise

    async def create_noetl_schema(self):
        try:
            logger.info(f"ATTEMPTING TO CREATE SCHEMA: Starting schema installation for '{self.noetl_schema}' with user '{self.noetl_user}'")
            logger.info(f"SCHEMA INSTALLATION: Using admin credentials to connect to database")

            try:
                self.admin_connection = await psycopg.AsyncConnection.connect(self.admin_conn)
                await self.admin_connection.set_autocommit(True)
                logger.info("SCHEMA INSTALLATION: Successfully connected to database with admin credentials (async)")
            except Exception as admin_conn_error:
                logger.error(f"SCHEMA INSTALLATION FAILED: Could not connect with admin credentials: {admin_conn_error}")
                logger.error("SCHEMA INSTALLATION FAILED: Make sure POSTGRES_USER and POSTGRES_PASSWORD environment variables are set correctly")
                raise

            async with self.admin_connection.cursor() as cursor:
                logger.info(f"SCHEMA INSTALLATION: Checking if user '{self.noetl_user}' exists...")
                await cursor.execute("""
                    SELECT 1 FROM pg_roles WHERE rolname = %s
                """, (self.noetl_user,))
                user_exists = await cursor.fetchone()

                if not user_exists:
                    logger.info(f"SCHEMA INSTALLATION: Creating user '{self.noetl_user}'...")
                    await cursor.execute(f"""
                        CREATE USER {self.noetl_user} WITH 
                        PASSWORD '{self.noetl_password}'
                        CREATEDB
                        LOGIN
                    """)
                    logger.info(f"SCHEMA INSTALLATION: User '{self.noetl_user}' created successfully")
                else:
                    logger.info(f"SCHEMA INSTALLATION: User '{self.noetl_user}' already exists")

                logger.info(f"SCHEMA INSTALLATION: Checking if schema '{self.noetl_schema}' exists...")
                await cursor.execute("""
                    SELECT 1 FROM information_schema.schemata WHERE schema_name = %s
                """, (self.noetl_schema,))
                schema_exists = await cursor.fetchone()

                if not schema_exists:
                    logger.info(f"SCHEMA INSTALLATION: Creating schema '{self.noetl_schema}'...")
                    await cursor.execute(f"CREATE SCHEMA {self.noetl_schema}")
                    logger.info(f"SCHEMA INSTALLATION: Schema '{self.noetl_schema}' created successfully")
                else:
                    logger.info(f"SCHEMA INSTALLATION: Schema '{self.noetl_schema}' already exists")

                logger.info(f"SCHEMA INSTALLATION: Granting permissions to user '{self.noetl_user}'...")
                await cursor.execute(f"GRANT ALL PRIVILEGES ON SCHEMA {self.noetl_schema} TO {self.noetl_user}")
                await cursor.execute(f"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA {self.noetl_schema} TO {self.noetl_user}")
                await cursor.execute(f"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA {self.noetl_schema} TO {self.noetl_user}")
                await cursor.execute(f"ALTER DEFAULT PRIVILEGES IN SCHEMA {self.noetl_schema} GRANT ALL ON TABLES TO {self.noetl_user}")
                await cursor.execute(f"ALTER DEFAULT PRIVILEGES IN SCHEMA {self.noetl_schema} GRANT ALL ON SEQUENCES TO {self.noetl_user}")

                await self.admin_connection.commit()
                logger.info("SCHEMA INSTALLATION: NoETL user and schema setup completed successfully")

        except Exception as e:
            logger.error(f"SCHEMA INSTALLATION FAILED: Error setting up noetl user and schema: {e}", exc_info=True)
            logger.error(f"SCHEMA INSTALLATION FAILED: Check that POSTGRES_USER has sufficient privileges to create users and schemas")
            logger.error(f"SCHEMA INSTALLATION FAILED: Verify that NOETL_USER, NOETL_PASSWORD, and NOETL_SCHEMA are correctly set")
            if self.admin_connection:
                await self.admin_connection.rollback()
            raise ValueError(f"Schema installation failed: {e}")
        finally:
            if self.admin_connection:
                await self.admin_connection.close()
                self.admin_connection = None

    async def init_database(self):
        try:
            logger.info("Initializing database tables (async).")
            await self.test_connection()
            await self.set_search_path()
            await self.create_postgres_tables()
            tables = await self.list_tables()
            logger.info(f"Tables in noetl schema: {tables}.")

            return True
        except Exception as e:
            logger.error(f"FATAL: Error initializing database: {e}.", exc_info=True)
            logger.error("FATAL: Exiting immediately due to database initialization failure")
            sys.exit(1)

    async def set_search_path(self):
        """Set the search path to use the noetl schema"""
        try:
            async with self.conn.cursor() as cursor:
                await cursor.execute(f"SET search_path TO {self.noetl_schema}, public")
                await self.conn.commit()
                logger.info(f"Search path set to '{self.noetl_schema}, public'")
        except Exception as e:
            logger.error(f"Error setting search path: {e}.", exc_info=True)
            raise

    async def test_connection(self):
        try:
            if self.conn is None:
                logger.error("FATAL: Database connection is None. Connection was not established properly.")
                raise ValueError("Database connection is None")

            async with self.conn.cursor() as cursor:
                await cursor.execute("SELECT 1")
                result = await cursor.fetchone()
                if result and result[0] == 1:
                    logger.info("Database connected (async).")
                else:
                    logger.error("FATAL: Database connection test failed.")
                    raise ValueError("Database connection test failed")
        except Exception as e:
            logger.error(f"FATAL: Error testing database connection: {e}.", exc_info=True)
            raise

    async def create_postgres_tables(self):
        try:
            ddl_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "database", "ddl", "postgres", "schema_ddl.sql"))
            if not os.path.isfile(ddl_path):
                raise FileNotFoundError(f"Canonical DDL file not found at {ddl_path}")
            async with self.conn.cursor() as cursor:
                with open(ddl_path, "r", encoding="utf-8") as f:
                    ddl_sql = f.read()
                await cursor.execute(ddl_sql)
                await self.conn.commit()
                logger.info("Executed canonical Postgres DDL from schema_ddl.sql (async).")
            return
        except Exception as e:
            logger.error(f"FATAL: Error creating postgres tables from canonical DDL (async): {e}", exc_info=True)
            raise

    async def list_tables(self) -> List[str]:
        try:
            async with self.conn.cursor() as cursor:
                await cursor.execute("""
                    SELECT table_name 
                    FROM information_schema.tables 
                    WHERE table_schema = %s
                """, (self.noetl_schema,))
                rows = await cursor.fetchall()
                return [row[0] for row in rows]
        except Exception as e:
            logger.error(f"Error listing tables: {e}.", exc_info=True)
            return []

    def table_exists(self, table_name: str) -> bool:
        try:
            with self.conn.cursor() as cursor:
                cursor.execute("""
                    SELECT EXISTS (
                        SELECT FROM information_schema.tables 
                        WHERE table_schema = %s AND table_name = %s
                    )
                """, (self.noetl_schema, table_name))
                return cursor.fetchone()[0]
        except Exception as e:
            logger.error(f"Error checking if table exists: {e}.", exc_info=True)
            return False

    def drop_noetl_schema(self):
        """Drop the entire noetl schema and all its objects"""
        try:
            target = (self.noetl_schema or "").strip()
            if not target:
                raise ValueError("NOETL_SCHEMA is empty or not set; refusing to drop schema")
            reserved = {"public", "pg_catalog", "information_schema"}
            if target.lower() in reserved:
                raise ValueError(f"Refusing to drop reserved schema '{target}'")
            logger.warning(f"[ADMIN] Dropping schema '{target}' and all its objects.")
            self.admin_connection = psycopg.connect(self.admin_conn)

            with self.admin_connection.cursor() as cursor:
                cursor.execute(f"DROP SCHEMA IF EXISTS {target} CASCADE")
                self.admin_connection.commit()
                logger.info(f"[ADMIN] Schema '{target}' dropped.")

        except Exception as e:
            logger.error(f"Error dropping schema: {e}.", exc_info=True)
            if self.admin_connection:
                self.admin_connection.rollback()
            raise
        finally:
            if self.admin_connection:
                self.admin_connection.close()
                self.admin_connection = None

    async def drop_noetl_schema_async(self):
        """Async drop of the entire noetl schema and all its objects using admin credentials"""
        try:
            target = (self.noetl_schema or "").strip()
            if not target:
                raise ValueError("NOETL_SCHEMA is empty or not set; refusing to drop schema")
            reserved = {"public", "pg_catalog", "information_schema"}
            if target.lower() in reserved:
                raise ValueError(f"Refusing to drop reserved schema '{target}'")

            logger.warning(f"[ADMIN] Dropping schema '{target}' and all its objects (async).")
            admin_conn = await psycopg.AsyncConnection.connect(self.admin_conn)
            try:
                await admin_conn.set_autocommit(True)
                async with admin_conn.cursor() as cursor:
                    await cursor.execute(f"DROP SCHEMA IF EXISTS {target} CASCADE")
                logger.info(f"[ADMIN] Schema '{target}' dropped (async).")
            finally:
                await admin_conn.close()
        except Exception as e:
            logger.error(f"Error dropping schema (async): {e}.", exc_info=True)
            raise

    def log_error(self, 
                error_type: str, 
                error_message: str, 
                execution_id: str = None, 
                step_id: str = None, 
                step_name: str = None,
                template_string: str = None, 
                context_data: Dict = None, 
                stack_trace: str = None, 
                input_data: Any = None, 
                output_data: Any = None,
                severity: str = "error") -> Optional[int]:
        """
        Log an error as an event into the unified event table.
        
        Args:
            error_type: The type of error (e.g., "template_rendering", "sql_rendering")
            error_message: The error message
            execution_id: The ID of the execution where the error occurred
            step_id: The ID of the step where the error occurred
            step_name: The name of the step where the error occurred
            template_string: The template that failed to render
            context_data: The context data used for rendering
            stack_trace: The stack trace of the error
            input_data: The input data related to the error
            output_data: The output data related to the error
            severity: The severity of the error (default: "error")
            
        Returns:
            The error_id of the inserted record, or None if insertion failed
        """
        try:
            if stack_trace is None and error_type == "template_rendering":
                stack_trace = ''.join(traceback.format_stack())
            
            context_data_json = json.dumps(make_serializable(context_data)) if context_data else None
            input_data_json = json.dumps(make_serializable(input_data)) if input_data else None
            output_data_json = json.dumps(make_serializable(output_data)) if output_data else None
            
            if not self.conn or self.conn.closed:
                self.initialize_connection_sync()
                
            with self.conn.cursor() as cursor:
                try:
                    from noetl.core.common import get_snowflake_id
                except Exception:
                    get_snowflake_id = lambda: int(datetime.datetime.now().timestamp() * 1000)
                event_id = get_snowflake_id()
                # Build metadata envelope (as text) for extra details
                meta_env = json.dumps(make_serializable({
                    'error_type': error_type,
                    'severity': severity,
                    'template_string': template_string,
                    'input_data': make_serializable(input_data),
                    'output_data': make_serializable(output_data)
                }))
                cursor.execute(f"""
                    INSERT INTO {self.noetl_schema}.event (
                        execution_id, event_id, created_at, event_type, node_id, node_name, status,
                        context, result, meta, error, stack_trace
                    ) VALUES (
                        %s, %s, CURRENT_TIMESTAMP, %s, %s, %s, %s,
                        %s, %s, %s, %s, %s
                    ) RETURNING event_id
                """, (
                    execution_id, event_id, 'error', step_id, step_name, 'FAILED',
                    context_data_json, output_data_json, meta_env, error_message, stack_trace
                ))
                inserted = cursor.fetchone()[0]
                self.conn.commit()
                logger.info(f"Logged error event with event_id: {inserted}")
                return inserted
                
        except Exception as e:
            logger.error(f"Failed to log error to error_log table: {e}", exc_info=True)
            if self.conn:
                try:
                    self.conn.rollback()
                except:
                    pass
            return None
    
    def mark_error_resolved(self, error_event_id: int, resolution_notes: str = None) -> bool:
        """Record an 'error_resolved' event in the unified event table."""
        try:
            if not self.conn or self.conn.closed:
                self.initialize_connection_sync()
            with self.conn.cursor() as cursor:
                cursor.execute(f"""
                    INSERT INTO {self.noetl_schema}.event (execution_id, event_id, created_at, event_type, status, meta)
                    VALUES (%s, %s, CURRENT_TIMESTAMP, 'error_resolved', 'COMPLETED', %s)
                """, (
                    0, error_event_id, json.dumps({'resolution_notes': resolution_notes}) if resolution_notes else None
                ))
                self.conn.commit()
                return True
        except Exception as e:
            logger.error(f"Database operation failed: {e}")
            logger.exception("Database operation exception details:")
            try:
                self.conn.rollback()
                logger.info("Transaction rolled back successfully")
            except Exception as rollback_e:
                logger.error(f"Critical: Transaction rollback failed: {rollback_e}")
                logger.exception("Rollback failure details:")
                # This is a serious issue - database may be in inconsistent state
            return False
    
    def get_errors(self, 
                  error_type: str = None, 
                  execution_id: str = None, 
                  resolved: bool = None, 
                  limit: int = 100, 
                  offset: int = 0) -> List[Dict]:
        """
        Get errors from the error_log table with optional filtering.
        
        Args:
            error_type: Filter by error type
            execution_id: Filter by execution ID
            resolved: Filter by resolved status
            limit: Maximum number of errors to return
            offset: Offset for pagination
            
        Returns:
            A list of error records as dictionaries
        """
        try:
            if not self.conn or self.conn.closed:
                self.initialize_connection_sync()
                
            query = f"SELECT execution_id, event_id, created_at, node_id, node_name, error, meta, stack_trace FROM {self.noetl_schema}.event WHERE event_type = 'error'"
            params = []
            if error_type:
                query += " AND meta LIKE %s"
                params.append(f'%"error_type": "{error_type}"%')
            if execution_id:
                query += " AND execution_id = %s"
                params.append(execution_id)
            # 'resolved' state is not a first-class column; skip
            query += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
            params.extend([limit, offset])
            
            with self.conn.cursor() as cursor:
                cursor.execute(query, params)
                rows = cursor.fetchall()
                errors: List[Dict[str, Any]] = []
                for exec_id, eid, ts, node_id, node_name, err, meta_txt, stack in rows:
                    try:
                        meta = json.loads(meta_txt) if isinstance(meta_txt, str) else (meta_txt or {})
                    except Exception:
                        meta = meta_txt or {}
                    errors.append({
                        'execution_id': exec_id,
                        'event_id': eid,
                        'timestamp': ts.isoformat() if hasattr(ts, 'isoformat') else ts,
                        'step_id': node_id,
                        'step_name': node_name,
                        'error_message': err,
                        'metadata': meta,
                        'stack_trace': stack,
                    })
                return errors
                
        except Exception as e:
            logger.error(f"Failed to get errors from error_log table: {e}", exc_info=True)
            return []
    
    # async def log_error_async(self, 
    #             error_type: str, 
    #             error_message: str, 
    #             execution_id: str = None, 
    #             step_id: str = None, 
    #             step_name: str = None,
    #             template_string: str = None,
    #             context_data: Dict = None,
    #             stack_trace: str = None,
    #             input_data: Any = None,
    #             output_data: Any = None,
    #             severity: str = "error") -> Optional[int]:
    #     try:
    #         if stack_trace is None and error_type == "template_rendering":
    #             stack_trace = ''.join(traceback.format_stack())

    #         context_data_json = json.dumps(make_serializable(context_data)) if context_data else None
    #         input_data_json = json.dumps(make_serializable(input_data)) if input_data else None
    #         output_data_json = json.dumps(make_serializable(output_data)) if output_data else None

    #         if not self.conn or getattr(self.conn, 'closed', False):
    #             await self.initialize_connection()

    #         async with self.conn.cursor() as cursor:
    #             try:
    #                 from noetl.core.common import get_snowflake_id
    #             except Exception:
    #                 get_snowflake_id = lambda: int(datetime.datetime.now().timestamp() * 1000)
    #             event_id = get_snowflake_id()
    #             meta_env = json.dumps(make_serializable({
    #                 'error_type': error_type,
    #                 'severity': severity,
    #                 'template_string': template_string,
    #                 'input_data': make_serializable(input_data),
    #                 'output_data': make_serializable(output_data)
    #             }))
    #             await cursor.execute(f"""
    #                 INSERT INTO {self.noetl_schema}.event (
    #                     execution_id, event_id, created_at, event_type, node_id, node_name, status,
    #                     context, result, meta, error, stack_trace
    #                 ) VALUES (
    #                     %s, %s, CURRENT_TIMESTAMP, %s, %s, %s, %s,
    #                     %s, %s, %s, %s, %s
    #                 ) RETURNING event_id
    #             """, (
    #                 execution_id, event_id, 'error', step_id, step_name, 'FAILED',
    #                 context_data_json, output_data_json, meta_env, error_message, stack_trace
    #             ))
    #             row = await cursor.fetchone()
    #             await self.conn.commit()
    #             event_row_id = row[0] if row else None
    #             logger.info(f"Logged error event with event_id: {event_row_id}")
    #             return event_row_id
    #     except Exception as e:
    #         logger.exception(f"Failed to log error (async): {e}")
    #         try:
    #             if self.conn and not getattr(self.conn, 'autocommit', False):
    #                 await self.conn.rollback()
    #         except Exception:
    #             pass
    #         return None

    async def mark_error_resolved_async(self, error_event_id: int, resolution_notes: str = None) -> bool:
        try:
            if not self.conn or getattr(self.conn, 'closed', False):
                await self.initialize_connection()
            async with self.conn.cursor() as cursor:
                await cursor.execute(f"""
                    INSERT INTO {self.noetl_schema}.event (execution_id, event_id, created_at, event_type, status, meta)
                    VALUES (%s, %s, CURRENT_TIMESTAMP, 'error_resolved', 'COMPLETED', %s)
                """, (
                    0, error_event_id, json.dumps({'resolution_notes': resolution_notes}) if resolution_notes else None
                ))
                await self.conn.commit()
                return True
        except Exception:
            try:
                if self.conn and not getattr(self.conn, 'autocommit', False):
                    await self.conn.rollback()
            except Exception:
                pass
            return False

    async def get_errors_async(self,
                  error_type: str = None,
                  execution_id: str = None,
                  resolved: bool = None,
                  limit: int = 100,
                  offset: int = 0) -> List[Dict]:
        try:
            if not self.conn or getattr(self.conn, 'closed', False):
                await self.initialize_connection()

            query = f"SELECT * FROM {self.noetl_schema}.event WHERE event_type = 'error'"
            params: list[Any] = []
            if error_type:
                query += " AND meta LIKE %s"
                params.append(f'%"error_type": "{error_type}"%')
            if execution_id:
                query += " AND execution_id = %s"
                params.append(execution_id)
            # resolved filter not supported in unified event table; encode in metadata if needed
            query += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
            params.extend([limit, offset])

            async with self.conn.cursor() as cursor:
                await cursor.execute(query, params)
                rows = await cursor.fetchall()
                columns = [desc[0] for desc in cursor.description]
                errors: List[Dict[str, Any]] = []
                for row in rows:
                    error_dict = dict(zip(columns, row))
                    # Back-compat: expose 'metadata' key sourced from 'meta' column if present
                    try:
                        if 'metadata' not in error_dict and 'meta' in error_dict:
                            error_dict['metadata'] = error_dict.get('meta')
                    except Exception:
                        pass
                    for field in ['context_data', 'input_data', 'output_data']:
                        if error_dict.get(field) and isinstance(error_dict[field], str):
                            try:
                                error_dict[field] = json.loads(error_dict[field])
                            except Exception:
                                pass
                    errors.append(error_dict)
                return errors
        except Exception as e:
            logger.error(f"Failed to get errors from error_log table (async): {e}", exc_info=True)
            return []

    async def close(self):
        if self.conn:
            try:
                await self.conn.close()
                logger.info("Database connection closed (async).")
            except Exception as e:
                logger.error(f"Error closing database connection: {e}.", exc_info=True)
