import json
from datetime import datetime

from app import db
from app.utils.timezone import now_in_app_timezone


class AuditLog(db.Model):
    """Audit log model for tracking detailed changes to entities

    Provides comprehensive audit trail tracking:
    - Who made the change (user_id)
    - What entity was changed (entity_type, entity_id)
    - When the change occurred (created_at)
    - What changed (field_name, old_value, new_value)
    - Action type (created, updated, deleted)
    - Additional context (ip_address, user_agent, request_path)
    """

    __tablename__ = "audit_logs"

    id = db.Column(db.Integer, primary_key=True)

    # User who made the change
    user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)

    # Entity being changed
    entity_type = db.Column(
        db.String(50), nullable=False, index=True
    )  # 'project', 'task', 'time_entry', 'invoice', 'client', 'user', etc.
    entity_id = db.Column(db.Integer, nullable=False, index=True)
    entity_name = db.Column(db.String(500), nullable=True)  # Cached name for display

    # Action details
    action = db.Column(
        db.String(20), nullable=False
    )  # 'created', 'updated', 'deleted' - index defined in __table_args__
    field_name = db.Column(
        db.String(100), nullable=True, index=True
    )  # Name of the field that changed (None for create/delete)

    # Change values (stored as JSON for flexibility)
    old_value = db.Column(db.Text, nullable=True)  # JSON-encoded old value
    new_value = db.Column(db.Text, nullable=True)  # JSON-encoded new value

    # Human-readable change description
    change_description = db.Column(db.Text, nullable=True)

    # User-provided reason for change/deletion
    reason = db.Column(db.Text, nullable=True)

    # Entity metadata (JSON-encoded for client_id, project_id, created_at, etc.)
    entity_metadata = db.Column(db.JSON, nullable=True)  # JSON metadata (dict/list)

    # Full entity state before change (JSON-encoded)
    full_old_state = db.Column(db.Text, nullable=True)

    # Full entity state after change (JSON-encoded)
    full_new_state = db.Column(db.Text, nullable=True)

    # Additional context
    ip_address = db.Column(db.String(45), nullable=True)
    user_agent = db.Column(db.Text, nullable=True)
    request_path = db.Column(db.String(500), nullable=True)

    # Timestamp
    created_at = db.Column(db.DateTime, default=now_in_app_timezone, nullable=False)  # index defined in __table_args__

    # Relationships
    user = db.relationship("User", backref="audit_logs")

    # Indexes for common queries
    __table_args__ = (
        db.Index("ix_audit_logs_entity", "entity_type", "entity_id"),
        db.Index("ix_audit_logs_user_created", "user_id", "created_at"),
        db.Index("ix_audit_logs_created_at", "created_at"),
        db.Index("ix_audit_logs_action", "action"),
    )

    def __repr__(self):
        return f"<AuditLog {self.action} {self.entity_type}#{self.entity_id} by user#{self.user_id}>"

    @classmethod
    def log_change(
        cls,
        user_id,
        action,
        entity_type,
        entity_id,
        field_name=None,
        old_value=None,
        new_value=None,
        entity_name=None,
        change_description=None,
        reason=None,
        entity_metadata=None,
        full_old_state=None,
        full_new_state=None,
        ip_address=None,
        user_agent=None,
        request_path=None,
    ):
        """Log a change to the audit trail

        Args:
            user_id: ID of the user making the change (None for system actions)
            action: 'created', 'updated', or 'deleted'
            entity_type: Type of entity (e.g., 'project', 'task', 'time_entry')
            entity_id: ID of the entity being changed
            field_name: Name of the field that changed (None for create/delete actions)
            old_value: Previous value (will be JSON-encoded)
            new_value: New value (will be JSON-encoded)
            entity_name: Cached name of the entity for display
            change_description: Human-readable description of the change
            reason: User-provided reason for the change/deletion
            entity_metadata: Metadata dict/list (client_id, project_id, created_at, etc.) - will be stored as JSON
            full_old_state: JSON-encoded full entity state before change
            full_new_state: JSON-encoded full entity state after change
            ip_address: IP address of the request
            user_agent: User agent string
            request_path: Path of the request that triggered the change
        """
        # Encode values as JSON if they're not already strings
        old_val_str = cls._encode_value(old_value)
        new_val_str = cls._encode_value(new_value)

        # entity_metadata is stored as JSON type, so pass dict/list directly (not encoded)
        # full_old_state and full_new_state are Text columns, so encode as JSON strings
        full_old_str = cls._encode_value(full_old_state) if full_old_state else None
        full_new_str = cls._encode_value(full_new_state) if full_new_state else None

        audit_log = cls(
            user_id=user_id,
            action=action,
            entity_type=entity_type,
            entity_id=entity_id,
            field_name=field_name,
            old_value=old_val_str,
            new_value=new_val_str,
            entity_name=entity_name,
            change_description=change_description,
            reason=reason,
            entity_metadata=entity_metadata,  # Pass dict/list directly for JSON column
            full_old_state=full_old_str,
            full_new_state=full_new_str,
            ip_address=ip_address,
            user_agent=user_agent,
            request_path=request_path,
        )

        try:
            # Add to session - don't commit here as we're likely in the middle of a transaction.
            # The main flush (or the explicit session.flush() in receive_after_flush for creates)
            # will persist the audit row.
            db.session.add(audit_log)
        except Exception as e:
            # Don't rollback - that would rollback the entire transaction including the main operation!
            # Just remove the audit log from the session and continue
            try:
                db.session.expunge(audit_log)
            except Exception as expunge_err:
                import logging
                logging.getLogger(__name__).debug("Audit log expunge failed: %s", expunge_err)
            # Don't let audit logging break the main flow
            # Log at warning level so it's visible if there's a real issue
            import logging

            logger = logging.getLogger(__name__)
            # Check if it's a table doesn't exist error
            error_str = str(e).lower()
            if (
                "does not exist" in error_str
                or "no such table" in error_str
                or "relation" in error_str
                and "does not exist" in error_str
            ):
                logger.warning(
                    f"audit_logs table does not exist - run migration 044_add_audit_logs_table.py. Error: {e}"
                )
            else:
                logger.warning(f"Failed to log audit change (non-critical): {e}")

    @staticmethod
    def _encode_value(value):
        """Encode a value as JSON string, handling None and special types"""
        if value is None:
            return None

        # Handle datetime objects
        if isinstance(value, datetime):
            return value.isoformat()

        # Handle Decimal and other types that aren't JSON serializable
        try:
            return json.dumps(value, default=str)
        except (TypeError, ValueError):
            return str(value)

    @staticmethod
    def _decode_value(value_str):
        """Decode a JSON string back to a Python value"""
        if value_str is None:
            return None

        try:
            return json.loads(value_str)
        except (json.JSONDecodeError, TypeError):
            # If it's not valid JSON, return as string
            return value_str

    def get_old_value(self):
        """Get the decoded old value"""
        return self._decode_value(self.old_value)

    def get_new_value(self):
        """Get the decoded new value"""
        return self._decode_value(self.new_value)

    def get_entity_metadata(self):
        """Get the entity metadata (already a dict/list for JSON column)"""
        # For JSON columns, SQLAlchemy returns the dict/list directly
        # For backward compatibility with Text columns, try to decode if it's a string
        if isinstance(self.entity_metadata, str):
            return self._decode_value(self.entity_metadata)
        return self.entity_metadata

    def get_full_old_state(self):
        """Get the decoded full old state"""
        return self._decode_value(self.full_old_state)

    def get_full_new_state(self):
        """Get the decoded full new state"""
        return self._decode_value(self.full_new_state)

    @classmethod
    def get_for_entity(cls, entity_type, entity_id, limit=100):
        """Get audit logs for a specific entity"""
        return (
            cls.query.filter_by(entity_type=entity_type, entity_id=entity_id)
            .order_by(cls.created_at.desc())
            .limit(limit)
            .all()
        )

    @classmethod
    def get_for_user(cls, user_id, limit=100):
        """Get audit logs for actions by a specific user"""
        return cls.query.filter_by(user_id=user_id).order_by(cls.created_at.desc()).limit(limit).all()

    @classmethod
    def get_recent(cls, limit=100, entity_type=None, user_id=None, action=None):
        """Get recent audit logs with optional filters"""
        query = cls.query

        if entity_type:
            query = query.filter_by(entity_type=entity_type)

        if user_id:
            query = query.filter_by(user_id=user_id)

        if action:
            query = query.filter_by(action=action)

        return query.order_by(cls.created_at.desc()).limit(limit).all()

    def to_dict(self):
        """Convert to dictionary for API responses"""
        return {
            "id": self.id,
            "user_id": self.user_id,
            "username": self.user.username if self.user else None,
            "display_name": self.user.display_name if self.user else None,
            "entity_type": self.entity_type,
            "entity_id": self.entity_id,
            "entity_name": self.entity_name,
            "action": self.action,
            "field_name": self.field_name,
            "old_value": self.get_old_value(),
            "new_value": self.get_new_value(),
            "change_description": self.change_description,
            "reason": self.reason,
            "entity_metadata": self.get_entity_metadata(),
            "full_old_state": self.get_full_old_state(),
            "full_new_state": self.get_full_new_state(),
            "ip_address": self.ip_address,
            "user_agent": self.user_agent,
            "request_path": self.request_path,
            "created_at": self.created_at.isoformat() if self.created_at else None,
        }

    def get_icon(self):
        """Get icon class for this audit log action"""
        icons = {
            "created": "fas fa-plus-circle text-green-500",
            "updated": "fas fa-edit text-blue-500",
            "deleted": "fas fa-trash text-red-500",
        }
        return icons.get(self.action, "fas fa-circle text-gray-500")

    def get_color(self):
        """Get color class for this audit log action"""
        colors = {
            "created": "green",
            "updated": "blue",
            "deleted": "red",
        }
        return colors.get(self.action, "gray")
