"""DataShield Schema Monitor — detect table schema changes and auto-update rules.

Captures point-in-time snapshots of table schemas and compares them to detect
added/removed columns and data type changes.
"""

import json
import logging
from datetime import datetime
from typing import Callable, Dict, List, Optional

from .types import (
    ColumnClassification,
    ColumnRule,
    ColumnSnapshot,
    SchemaChangeEvent,
    SchemaSnapshot,
    TableShieldConfig,
    CLASSIFICATION_STRATEGY_MAP,
)

logger = logging.getLogger(__name__)


def capture_snapshot(client, database: str, schema: str, table: str) -> SchemaSnapshot:
    """Capture a point-in-time snapshot of a table's schema.

    Args:
        client: SnowflakeClient instance
        database: Database name
        schema: Schema name
        table: Table name

    Returns:
        SchemaSnapshot with all column metadata
    """
    columns_raw = client.get_columns(database, schema, table)
    columns = []
    for i, c in enumerate(columns_raw):
        data_type = c.get("type", "VARCHAR")
        # Snowflake may return JSON-encoded type info
        try:
            dt_obj = json.loads(data_type)
            data_type = dt_obj.get("type", data_type)
        except (json.JSONDecodeError, TypeError, AttributeError):
            pass

        nullable_raw = c.get("nullable", "YES")
        nullable = nullable_raw in (True, "YES", "true", "Y")

        columns.append(ColumnSnapshot(
            name=c.get("name", ""),
            data_type=data_type,
            nullable=nullable,
            ordinal_position=i,
        ))

    return SchemaSnapshot(
        database=database,
        schema_name=schema,
        table_name=table,
        columns=columns,
        captured_at=datetime.now().isoformat(),
    )


def compare_schemas(
    old_snapshot: SchemaSnapshot,
    new_snapshot: SchemaSnapshot,
) -> List[SchemaChangeEvent]:
    """Compare two schema snapshots and return detected changes.

    Args:
        old_snapshot: Previously captured snapshot (baseline)
        new_snapshot: Current snapshot from Snowflake

    Returns:
        List of SchemaChangeEvent (empty if schemas match)
    """
    changes: List[SchemaChangeEvent] = []
    now = datetime.now().isoformat()

    old_cols = {c.name: c for c in old_snapshot.columns}
    new_cols = {c.name: c for c in new_snapshot.columns}

    # Detect added columns
    for name in new_cols:
        if name not in old_cols:
            changes.append(SchemaChangeEvent(
                detected_at=now,
                change_type="column_added",
                column_name=name,
                old_value=None,
                new_value=new_cols[name].data_type,
            ))

    # Detect removed columns
    for name in old_cols:
        if name not in new_cols:
            changes.append(SchemaChangeEvent(
                detected_at=now,
                change_type="column_removed",
                column_name=name,
                old_value=old_cols[name].data_type,
                new_value=None,
            ))

    # Detect type changes on columns that exist in both
    for name in old_cols:
        if name in new_cols:
            old_type = old_cols[name].data_type.upper()
            new_type = new_cols[name].data_type.upper()
            if old_type != new_type:
                changes.append(SchemaChangeEvent(
                    detected_at=now,
                    change_type="type_changed",
                    column_name=name,
                    old_value=old_cols[name].data_type,
                    new_value=new_cols[name].data_type,
                ))

    return changes


def apply_changes(
    table_config: TableShieldConfig,
    changes: List[SchemaChangeEvent],
    classifier_fn: Optional[Callable] = None,
) -> Dict:
    """Apply detected schema changes to a table's shield configuration.

    Args:
        table_config: Existing table shield config to update
        changes: List of detected changes from compare_schemas
        classifier_fn: Optional callable(column_name, data_type) -> ColumnClassification
            Used for AI or rule-based classification of new columns.
            If None, defaults to SAFE.

    Returns:
        Summary dict with keys: changes_applied, new_rules, removed_rules, needs_redeploy
    """
    new_rules = []
    removed_rules = []
    existing_rule_map = {r.column_name: r for r in table_config.column_rules}

    for change in changes:
        if change.change_type == "column_added":
            # Classify new column
            classification = ColumnClassification.SAFE
            if classifier_fn:
                try:
                    classification = classifier_fn(change.column_name, change.new_value or "VARCHAR")
                except Exception as e:
                    logger.warning("Classifier failed for %s: %s", change.column_name, e)

            strategy = CLASSIFICATION_STRATEGY_MAP.get(classification)
            rule = ColumnRule(
                column_name=change.column_name,
                classification=classification,
                strategy=strategy,
                preserve_nulls=True,
                preserve_format=True,
            )
            table_config.column_rules.append(rule)
            new_rules.append(change.column_name)

        elif change.change_type == "column_removed":
            # Remove the rule for the dropped column
            before = len(table_config.column_rules)
            table_config.column_rules = [
                r for r in table_config.column_rules
                if r.column_name != change.column_name
            ]
            if len(table_config.column_rules) < before:
                removed_rules.append(change.column_name)
            # Also remove from key_columns / skip_columns
            if change.column_name in table_config.key_columns:
                table_config.key_columns.remove(change.column_name)
            if change.column_name in table_config.skip_columns:
                table_config.skip_columns.remove(change.column_name)

        elif change.change_type == "type_changed":
            rule = existing_rule_map.get(change.column_name)
            if rule and classifier_fn:
                try:
                    new_cls = classifier_fn(change.column_name, change.new_value or "VARCHAR")
                    rule.classification = new_cls
                    rule.strategy = CLASSIFICATION_STRATEGY_MAP.get(new_cls)
                except Exception as e:
                    logger.warning("Re-classify failed for %s: %s", change.column_name, e)

    # Record changes in history
    table_config.change_history.extend(changes)

    return {
        "changes_applied": len(changes),
        "new_rules": new_rules,
        "removed_rules": removed_rules,
        "needs_redeploy": len(changes) > 0,
    }
