import psycopg2
from psycopg2 import sql, Error
from typing import List, Optional, Dict, Type
import datetime
from datetime import date
from .fields.psql import (
    Field, DateTimeField, DecimalField, TimeField, DateField, 
    CharField, ForeignKey, EmailField, URLField, BooleanField
)


class RelatedManager:
    """Manager for handling related objects from ForeignKey"""
    
    def __init__(self, model, field_name, instance_id):
        self.model = model
        self.field_name = field_name
        self.instance_id = instance_id

    def all(self):
        """Get all related objects"""
        return self.model.filter(**{self.field_name: self.instance_id})

    def filter(self, **kwargs):
        """Filter related objects"""
        return self.model.filter(**{self.field_name: self.instance_id, **kwargs})

    def first(self):
        """Get first related object"""
        qs = self.all()
        return qs.first() if qs.exists() else None

    def last(self):
        """Get last related object"""
        qs = self.all()
        return qs.last() if qs.exists() else None

    def count(self):
        """Count related objects"""
        return self.all().count()
    
    def to_dict(self) -> List[Dict]:
        """Convert related objects to list of dicts"""
        return self.all().to_dict()


class ModelMeta(type):
    """Metaclass for automatic table creation and related_name setup"""
    
    def __new__(cls, name, bases, dct):
        new_cls = super().__new__(cls, name, bases, dct)

        # Set table name
        if not hasattr(new_cls.Meta, 'table_name') or not new_cls.Meta.table_name:
            new_cls.table_name = name.lower()
        else:
            new_cls.table_name = new_cls.Meta.table_name

        for attr, field in dct.items():
            if isinstance(field, ForeignKey) and field.related_name:
                def create_related_manager(source_model, source_field):
                    """Factory function to create related manager property"""
                    def get_manager(self):
                        return RelatedManager(source_model, source_field, self.id)
                    return property(get_manager)
                
                # Set property on the related model
                setattr(field.to, field.related_name, create_related_manager(new_cls, attr))

        # Auto-create table if db_config exists
        if hasattr(new_cls.Meta, 'db_config') and new_cls.Meta.db_config:
            new_cls.create_table()

        return new_cls

class BaseModel(metaclass=ModelMeta):
    """Base model class for PostgreSQL ORM"""
    
    table_name = ''
    
    class QuerySet:
        """QuerySet for handling query results"""
        
        def __init__(self, results, total_count, page, page_size):
            self.results = results
            self.total_count = total_count
            self.page = page
            self.page_size = page_size
        
        def filter(self, **kwargs) -> 'QuerySet':
            """Filter QuerySet results in-memory"""
            if not kwargs:
                raise ValueError("At least one filter must be provided")

            filtered_results = []

            for obj in self.results:
                match = True
                for key, value in kwargs.items():
                    if '__' in key:
                        field_name, op = key.split('__', 1)
                        field_value = getattr(obj, field_name, None)
                        
                        if op == 'gte' and not (field_value >= value):
                            match = False
                            break
                        elif op == 'lte' and not (field_value <= value):
                            match = False
                            break
                        elif op == 'gt' and not (field_value > value):
                            match = False
                            break
                        elif op == 'lt' and not (field_value < value):
                            match = False
                            break
                        elif op == 'exact' and not (field_value == value):
                            match = False
                            break
                        else:
                            raise ValueError(f"Unsupported filter operator: {op}")
                    else:
                        if getattr(obj, key, None) != value:
                            match = False
                            break

                if match:
                    filtered_results.append(obj)

            return self.__class__(filtered_results, len(filtered_results), self.page, self.page_size)
        
        def count(self) -> int:
            """Count results"""
            return len(self.results)

        def to_dict(self) -> List[Dict]:
            """Convert results to list of dictionaries"""
            return [obj.__dict__ for obj in self.results]
        
        def __repr__(self):
            """String representation"""
            sample = self.to_dict()[:3]
            return f"<QuerySet(count={self.count()}, first_3_items={sample})>"
        
        def order_by(self, field: str):
            """Order results by field"""
            reverse = field.startswith('-')
            field_name = field.lstrip('-')
            
            try:
                sorted_results = sorted(
                    self.results,
                    key=lambda obj: getattr(obj, field_name),
                    reverse=reverse
                )
            except AttributeError:
                raise ValueError(f"Field '{field_name}' does not exist")
            
            return self.__class__(sorted_results, self.total_count, self.page, self.page_size)
        
        def first(self):
            """Get first result"""
            return self.results[0] if self.results else None

        def last(self):
            """Get last result"""
            return self.results[-1] if self.results else None
        
        def exists(self) -> bool:
            """Check if results exist"""
            return bool(self.results)

        def paginate(self, page: int, page_size: int):
            """Paginate results"""
            if page < 1:
                raise ValueError("Page number must be >= 1")
            if page_size < 1:
                raise ValueError("Page size must be >= 1")
            
            offset = (page - 1) * page_size
            paginated_results = self.results[offset:offset + page_size]
            return self.__class__(paginated_results, self.total_count, page, page_size)
        
        def contains(self, **kwargs) -> 'QuerySet':
            """Case-insensitive contains search"""
            if not kwargs:
                raise ValueError("At least one field must be provided")

            filtered_results = []

            for obj in self.results:
                match = True
                for field, value in kwargs.items():
                    field_value = getattr(obj, field, None)
                    
                    if isinstance(field_value, str):
                        if str(value).lower() not in field_value.lower():
                            match = False
                            break
                    elif isinstance(field_value, (int, float)):
                        if str(value) not in str(field_value):
                            match = False
                            break
                    elif isinstance(field_value, (datetime.datetime, datetime.date)):
                        if str(value) not in field_value.strftime('%Y-%m-%d'):
                            match = False
                            break
                    elif field_value != value:
                        match = False
                        break

                if match:
                    filtered_results.append(obj)

            return self.__class__(filtered_results, len(filtered_results), self.page, self.page_size)

    def __repr__(self):
        """String representation of model instance"""
        field_values = {
            attr: getattr(self, attr, None) 
            for attr in self.__class__.__dict__ 
            if isinstance(self.__class__.__dict__[attr], Field)
        }
        return f"<{self.__class__.__name__} {field_values}>"
    
    class Meta:
        db_config = {}

    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classmethod
    def connect(cls):
        """Create database connection"""
        if not hasattr(cls, 'Meta') or not hasattr(cls.Meta, 'db_config'):
            raise AttributeError(
                f"Class {cls.__name__} must define 'Meta.db_config' with database connection details"
            )

        db_config = cls.Meta.db_config

        try:
            return psycopg2.connect(
                host=db_config['host'],
                user=db_config['user'],
                password=db_config['password'],
                database=db_config['database'],
                port=db_config['port']
            )
        except psycopg2.OperationalError as e:
            if "does not exist" in str(e):
                cls._create_database()
                return psycopg2.connect(
                    host=db_config['host'],
                    user=db_config['user'],
                    password=db_config['password'],
                    database=db_config['database'],
                    port=db_config['port']
                )
            else:
                raise ConnectionError(f"Error connecting to PostgreSQL database: {e}")

    @classmethod
    def _create_database(cls):
        """Create database if it doesn't exist"""
        try:
            db_config = cls.Meta.db_config
            conn = psycopg2.connect(
                host=db_config['host'],
                user=db_config['user'],
                password=db_config['password'],
                database="postgres",
                port=db_config['port']
            )
            conn.autocommit = True
            cursor = conn.cursor()
            
            db_name = db_config['database']

            cursor.execute(
                "SELECT 1 FROM pg_catalog.pg_database WHERE datname = %s",
                (db_name,)
            )
            if not cursor.fetchone():
                cursor.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(db_name)))
                print(f"Database '{db_name}' created successfully.")
            else:
                print(f"Database '{db_name}' already exists.")

            cursor.close()
            conn.close()
        except Exception as e:
            raise ConnectionError(f"Error creating PostgreSQL database: {e}")

    @classmethod
    def create_table(cls):
        """Create database table with foreign key constraints"""
        conn = cls.connect()
        cursor = None
        
        try:
            cursor = conn.cursor()
            
            # Step 1: Create table if not exists
            try:
                columns = cls._get_column_definitions()
                create_sql = f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id SERIAL PRIMARY KEY, {', '.join(columns)})"
                cursor.execute(create_sql)
                conn.commit()
            except psycopg2.Error as e:
                conn.rollback()
                if "already exists" not in str(e):
                    print(f"Warning during table creation: {e}")
                cursor.close()
                cursor = conn.cursor()
            
            # Step 2: Add foreign key constraints
            try:
                cls._add_foreign_key_constraints(cursor)
                conn.commit()
            except psycopg2.Error as e:
                conn.rollback()
                if "already exists" not in str(e):
                    print(f"Warning during foreign key creation: {e}")
                cursor.close()
                cursor = conn.cursor()
            
            # Step 3: Update table structure (add new columns)
            try:
                cls._update_table_structure(cursor)
                conn.commit()
            except psycopg2.Error as e:
                conn.rollback()
                print(f"Warning during table update: {e}")
                
        except Exception as e:
            if conn:
                try:
                    conn.rollback()
                except:
                    pass
            raise ConnectionError(f"Failed to create table {cls.table_name}: {e}")
        
        finally:
            if cursor:
                try:
                    cursor.close()
                except:
                    pass
            if conn:
                try:
                    conn.close()
                except:
                    pass

    @classmethod
    def _get_column_definitions(cls):
        """Generate column definitions for CREATE TABLE"""
        columns = []
        
        for attr, field in cls.__dict__.items():
            if not isinstance(field, Field):
                continue
            
            # Skip ForeignKey constraint definition here (added separately)
            col_type = field.field_type
            
            if isinstance(field, DecimalField):
                col_type = f"DECIMAL({field.max_digits}, {field.decimal_places})"
            elif isinstance(field, CharField):
                col_type = f"VARCHAR({field.max_length})"
            
            column_def = f"{attr} {col_type}"
            
            if field.unique:
                column_def += " UNIQUE"
            
            if not field.null:
                column_def += " NOT NULL"
            
            if field.default is not None:
                if isinstance(field, BooleanField):
                    column_def += f" DEFAULT {field.set_default()}"
                elif isinstance(field.default, str):
                    column_def += f" DEFAULT '{field.default}'"
                else:
                    column_def += f" DEFAULT {field.default}"
            
            columns.append(column_def)
        
        return columns

    @classmethod
    def _add_foreign_key_constraints(cls, cursor):
        """Add FOREIGN KEY constraints after table creation"""
        for attr, field in cls.__dict__.items():
            if isinstance(field, ForeignKey):
                try:
                    constraint_name = f"fk_{cls.table_name}_{attr}"
                    cursor.execute(
                        """
                        SELECT 1 FROM information_schema.table_constraints 
                        WHERE constraint_name = %s AND table_name = %s
                        """,
                        (constraint_name, cls.table_name)
                    )
                    
                    if cursor.fetchone():
                        # Constraint already exists
                        continue
                    
                    constraint_sql = field.get_constraint(attr, cls.table_name)
                    cursor.execute(constraint_sql)
                    
                except psycopg2.Error as e:
                    if "already exists" in str(e) or "duplicate" in str(e).lower():
                        continue
                    else:
                        print(f"Warning: Could not add foreign key constraint for {attr}: {e}")

    @classmethod
    def _update_table_structure(cls, cursor):
        """Add new columns to existing table"""
        existing_columns = cls._get_existing_columns(cursor)
        new_columns = [
            attr for attr in cls.__dict__ 
            if isinstance(cls.__dict__[attr], Field) and attr not in existing_columns
        ]

        for column in new_columns:
            field = cls.__dict__[column]
            col_type = field.field_type
            
            if isinstance(field, DecimalField):
                col_type = f"DECIMAL({field.max_digits}, {field.decimal_places})"
            elif isinstance(field, CharField):
                col_type = f"VARCHAR({field.max_length})"
            
            column_def = f"ALTER TABLE {cls.table_name} ADD COLUMN {column} {col_type}"
            
            # For new columns, allow NULL initially
            column_def += " NULL"
            
            if field.default is not None:
                if isinstance(field, BooleanField):
                    column_def += f" DEFAULT {field.set_default()}"
                elif isinstance(field.default, str):
                    column_def += f" DEFAULT '{field.default}'"
                else:
                    column_def += f" DEFAULT {field.default}"

            try:
                cursor.execute(column_def)
            except psycopg2.Error as e:
                if "already exists" in str(e).lower() or "duplicate" in str(e).lower():
                    # Column already exists, skip
                    continue
                else:
                    print(f"Warning: Could not add column {column}: {e}")

    @classmethod
    def _get_existing_columns(cls, cursor):
        """Get existing columns from table"""
        cursor.execute(
            "SELECT column_name FROM information_schema.columns WHERE table_name = %s",
            (cls.table_name,)
        )
        return {row[0] for row in cursor.fetchall()}
    
    @classmethod
    def _get_valid_fields(cls):
        """Cache valid field names for performance"""
        if not hasattr(cls, '_valid_fields_cache'):
            cls._valid_fields_cache = {
                attr for attr, field in cls.__dict__.items() 
                if isinstance(field, Field)
            }
            cls._valid_fields_cache.add('id')
        return cls._valid_fields_cache
    
    @classmethod
    def _validate_and_convert_values(cls, **kwargs):
        """Validate and convert field values using field validators"""
        validated = {}
        
        for attr, field in cls.__dict__.items():
            if not isinstance(field, Field):
                continue
            
            # Handle ForeignKey
            if isinstance(field, ForeignKey):
                if attr in kwargs:
                    try:
                        validated[attr] = field.validate(kwargs[attr])
                    except ValueError as e:
                        raise ValueError(f"Validation error for field '{attr}': {e}")
                continue
            
            # Handle regular fields
            if attr in kwargs:
                # Apply field validation if method exists
                if hasattr(field, 'validate'):
                    try:
                        validated[attr] = field.validate(kwargs[attr])
                    except ValueError as e:
                        raise ValueError(f"Validation error for field '{attr}': {e}")
                else:
                    validated[attr] = kwargs[attr]
            
            # Handle auto_now and auto_now_add
            elif isinstance(field, (DateField, DateTimeField)):
                if field.auto_now or field.auto_now_add:
                    if isinstance(field, DateField):
                        validated[attr] = datetime.datetime.now().date()
                    else:
                        validated[attr] = datetime.datetime.now()
        
        return validated
    
    @classmethod
    def all(cls, order_by: Optional[str] = None) -> 'QuerySet':
        """Get all records"""
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                query = f"SELECT * FROM {cls.table_name}"
                
                if order_by:
                    field_name = order_by.lstrip('-')
                    valid_fields = cls._get_valid_fields()
                    if field_name != 'id' and field_name not in valid_fields:
                        raise ValueError(f"Invalid field name for ordering: {field_name}")
                    
                    direction = "DESC" if order_by.startswith('-') else "ASC"
                    query += f" ORDER BY {field_name} {direction}"
                
                cursor.execute(query)
                results = cursor.fetchall()
                
                return cls.QuerySet(
                    [cls(**dict(zip([c[0] for c in cursor.description], row))) for row in results],
                    len(results),
                    page=1,
                    page_size=len(results)
                )
        finally:
            conn.close()

    @classmethod
    def filter(cls, **kwargs) -> 'QuerySet':
        """Filter records with various operators"""
        if not kwargs:
            return cls.all()
        
        conditions = []
        values = []
        valid_fields = cls._get_valid_fields()

        for key, value in kwargs.items():
            base_key = key
            operator = "="
            
            if key.endswith("__gte"):
                base_key = key[:-5]
                operator = ">="
            elif key.endswith("__lte"):
                base_key = key[:-5]
                operator = "<="
            elif key.endswith("__gt"):
                base_key = key[:-4]
                operator = ">"
            elif key.endswith("__lt"):
                base_key = key[:-4]
                operator = "<"
            elif key.endswith("__ne"):
                base_key = key[:-4]
                operator = "!="
            elif key.endswith("__icontains"):
                base_key = key[:-11]
                if base_key != 'id' and base_key not in valid_fields:
                    raise ValueError(f"Invalid field name: {base_key}")
                conditions.append(f"{base_key} ILIKE %s")
                values.append(f"%{value}%")
                continue
            elif key.endswith("__contains"):
                base_key = key[:-10]
                if base_key != 'id' and base_key not in valid_fields:
                    raise ValueError(f"Invalid field name: {base_key}")
                conditions.append(f"{base_key} LIKE %s")
                values.append(f"%{value}%")
                continue
            elif key.endswith("__in"):
                base_key = key[:-4]
                if not isinstance(value, (list, tuple)):
                    raise ValueError(f"Value for {key} must be a list or tuple")
                if base_key != 'id' and base_key not in valid_fields:
                    raise ValueError(f"Invalid field name: {base_key}")
                placeholders = ", ".join(["%s" for _ in value])
                conditions.append(f"{base_key} IN ({placeholders})")
                values.extend(value)
                continue
            
            if base_key != 'id' and base_key not in valid_fields:
                raise ValueError(f"Invalid field name: {base_key}")
            
            conditions.append(f"{base_key} {operator} %s")
            values.append(value)

        query = f"SELECT * FROM {cls.table_name} WHERE " + " AND ".join(conditions)
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                cursor.execute(query, tuple(values))
                results = cursor.fetchall()
                
                return cls.QuerySet(
                    [cls(**dict(zip([c[0] for c in cursor.description], row))) for row in results],
                    len(results),
                    page=1,
                    page_size=len(results)
                )
        finally:
            conn.close()

    @classmethod
    def get(cls, **kwargs) -> Optional['BaseModel']:
        """Get single record"""
        if not kwargs:
            raise ValueError("At least one filter must be provided")
        
        valid_fields = cls._get_valid_fields()
        
        for key in kwargs.keys():
            if key != 'id' and key not in valid_fields:
                raise ValueError(f"Invalid field name: {key}")
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                query = f"SELECT * FROM {cls.table_name} WHERE " + " AND ".join([f"{k} = %s" for k in kwargs.keys()])
                cursor.execute(query, tuple(kwargs.values()))
                result = cursor.fetchone()
                
                if result:
                    return cls(**dict(zip([c[0] for c in cursor.description], result)))
                return None
        finally:
            conn.close()

    @classmethod
    def create(cls, **kwargs) -> int:
        """Create new record with validation"""
        # Validate and convert all values
        validated_data = cls._validate_and_convert_values(**kwargs)
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                columns = list(validated_data.keys())
                placeholders = ['%s' for _ in columns]
                values = list(validated_data.values())
                
                query = f"INSERT INTO {cls.table_name} ({', '.join(columns)}) VALUES ({', '.join(placeholders)}) RETURNING id"
                cursor.execute(query, tuple(values))
                new_id = cursor.fetchone()[0]
            
            conn.commit()
            return new_id
        finally:
            conn.close()
    
    @classmethod
    def bulk_create(cls, records: list) -> int:
        """Bulk create records with validation"""
        if not records:
            raise ValueError("The records list is empty")
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                all_values = []
                
                for record in records:
                    validated_data = cls._validate_and_convert_values(**record)
                    all_values.append(tuple(validated_data.values()))
                
                # Use columns from first validated record
                first_validated = cls._validate_and_convert_values(**records[0])
                columns = list(first_validated.keys())
                placeholders = ", ".join(["%s" for _ in columns])
                query = f"INSERT INTO {cls.table_name} ({', '.join(columns)}) VALUES ({placeholders})"
                
                cursor.executemany(query, all_values)
            
            conn.commit()
            return len(all_values)
        finally:
            conn.close()

    def save(self):
        """Save instance (insert or update)"""
        if hasattr(self, 'id') and self.id:
            data = self.__dict__.copy()
            data.pop('id', None)
            try:
                self.__class__.update(self.id, **data)
            except ValueError as e:
                raise ValueError(f"Cannot save: {e}")
        else:
            data = self.__dict__.copy()
            data.pop('id', None)
            new_id = self.__class__.create(**data)
            self.id = new_id

    @classmethod
    def update(cls, id: int, **kwargs) -> bool:
        """Update record with validation"""
        if not kwargs:
            raise ValueError("At least one field to update must be provided")
        
        if 'id' in kwargs:
            raise ValueError("Cannot update 'id' field")
        
        # Validate fields
        validated_data = cls._validate_and_convert_values(**kwargs)
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                set_clause = ', '.join([f"{k} = %s" for k in validated_data.keys()])
                values = list(validated_data.values())
                
                cursor.execute(
                    f"UPDATE {cls.table_name} SET {set_clause} WHERE id = %s",
                    (*values, id)
                )
                updated_rows = cursor.rowcount
            
            conn.commit()
            
            if updated_rows == 0:
                raise ValueError(f"No record found with id={id}")
            
            return True
        finally:
            conn.close()
        
    @classmethod
    def delete(cls, **filters) -> int:
        """Delete records"""
        if not filters:
            raise ValueError("At least one filter must be specified")
        
        valid_fields = cls._get_valid_fields()
        
        for key in filters.keys():
            if key != 'id' and key not in valid_fields:
                raise ValueError(f"Invalid field name: {key}")
        
        conn = cls.connect()
        try:
            with conn.cursor() as cursor:
                where_clause = " AND ".join(f"{key} = %s" for key in filters.keys())
                values = tuple(filters.values())
                query = f"DELETE FROM {cls.table_name} WHERE {where_clause}"
                
                cursor.execute(query, values)
                deleted_rows = cursor.rowcount
            
            conn.commit()
            return deleted_rows
        finally:
            conn.close()


class PostgreSQLModel(BaseModel):
    """PostgreSQL model class"""
    class Meta:
        db_config = {}
