def detect_task_type(model):
    """Detect classification vs regression from the model object."""
    # Try sklearn mixin check
    try:
        from sklearn.base import ClassifierMixin, RegressorMixin
        if isinstance(model, ClassifierMixin):
            return 'classification'
        if isinstance(model, RegressorMixin):
            return 'regression'
    except ImportError:
        pass

    # Fallback: check class name heuristics
    cls_name = type(model).__name__.lower()
    classifier_keywords = ('classifier', 'logistic', 'svc', 'sgd', 'nb', 'bayes')
    regressor_keywords = ('regressor', 'regression', 'svr', 'lasso', 'ridge', 'elasticnet')
    if any(kw in cls_name for kw in classifier_keywords):
        return 'classification'
    if any(kw in cls_name for kw in regressor_keywords):
        return 'regression'

    # Check for classes_ attribute (classifiers have this)
    if hasattr(model, 'classes_'):
        return 'classification'

    # Keras models: inspect output layer activation and loss function
    if _is_keras_model(model):
        return _detect_keras_task_type(model)

    return None


def _detect_keras_task_type(model):
    """Detect task type from a Keras model's output layer and loss."""
    try:
        # Check loss function first (most reliable)
        loss = getattr(model, 'loss', None)
        if loss is not None:
            loss_name = loss if isinstance(loss, str) else getattr(loss, '__name__', str(loss))
            loss_name = loss_name.lower()
            if any(kw in loss_name for kw in ('crossentropy', 'hinge', 'focal')):
                return 'classification'
            if any(kw in loss_name for kw in ('mse', 'mae', 'huber', 'squared', 'absolute')):
                return 'regression'

        # Check output layer activation
        layers = getattr(model, 'layers', [])
        if layers:
            last_layer = layers[-1]
            activation = getattr(last_layer, 'activation', None)
            if activation is not None:
                act_name = getattr(activation, '__name__', str(activation)).lower()
                if act_name in ('softmax', 'sigmoid'):
                    return 'classification'
                if act_name == 'linear':
                    return 'regression'
    except Exception:
        pass
    return None


def detect_framework(model):
    """Detect the ML framework from the model's module path."""
    module = getattr(type(model), '__module__', '') or ''
    for name in ('sklearn', 'xgboost', 'lightgbm', 'catboost', 'tensorflow', 'keras', 'torch'):
        if name in module:
            if name == 'torch':
                return 'pytorch'
            if name == 'keras':
                return 'tensorflow'
            return name
    return None


def _is_keras_model(model):
    """Check if a model is a Keras model without importing TensorFlow."""
    module = getattr(type(model), '__module__', '') or ''
    return 'keras' in module


def detect_hyperparameters(model):
    """Extract hyperparameters via get_params() if available."""
    if hasattr(model, 'get_params'):
        try:
            params = model.get_params()
            # Ensure all values are JSON-serializable
            return {k: _make_serializable(v) for k, v in params.items()}
        except Exception:
            pass

    # Keras models: extract optimizer config, loss, architecture summary
    if _is_keras_model(model):
        return _detect_keras_hyperparameters(model)

    return {}


def _detect_keras_hyperparameters(model):
    """Extract hyperparameters from a Keras model."""
    params = {}
    try:
        # Optimizer
        optimizer = getattr(model, 'optimizer', None)
        if optimizer is not None:
            params['optimizer'] = type(optimizer).__name__
            try:
                opt_config = optimizer.get_config()
                params['learning_rate'] = _make_serializable(
                    opt_config.get('learning_rate', opt_config.get('lr'))
                )
            except Exception:
                pass

        # Loss
        loss = getattr(model, 'loss', None)
        if loss is not None:
            params['loss'] = loss if isinstance(loss, str) else getattr(loss, '__name__', str(loss))

        # Architecture summary
        params['total_params'] = int(model.count_params()) if hasattr(model, 'count_params') else None
        params['num_layers'] = len(model.layers) if hasattr(model, 'layers') else None

        # Input shape
        input_shape = getattr(model, 'input_shape', None)
        if input_shape is not None:
            params['input_shape'] = _make_serializable(input_shape)
    except Exception:
        pass
    return params


def detect_class_labels(model):
    """Extract class labels from classifier models."""
    if hasattr(model, 'classes_'):
        try:
            return [str(c) for c in model.classes_]
        except Exception:
            pass
    return None


def detect_n_features(model):
    """Extract feature count from fitted model."""
    if hasattr(model, 'n_features_in_'):
        try:
            return int(model.n_features_in_)
        except Exception:
            pass
    # Keras: get from input_shape
    if _is_keras_model(model):
        try:
            input_shape = model.input_shape
            if isinstance(input_shape, (tuple, list)) and len(input_shape) >= 2:
                return int(input_shape[-1])
        except Exception:
            pass
    return None


def detect_feature_names(model):
    """Extract feature names if available (sklearn 1.0+)."""
    if hasattr(model, 'feature_names_in_'):
        try:
            return list(model.feature_names_in_)
        except Exception:
            pass
    return None


def detect_model_class(model):
    """Return the model class name (e.g. 'RandomForestClassifier')."""
    return type(model).__name__


def detect_all(model, task_type=None, framework=None):
    """Run all auto-detection and return a metadata dict.

    User-provided values take precedence over auto-detected ones.
    """
    return {
        'task_type': task_type or detect_task_type(model),
        'framework': framework or detect_framework(model),
        'model_class': detect_model_class(model),
        'hyperparameters': detect_hyperparameters(model),
        'class_labels': detect_class_labels(model),
        'n_features': detect_n_features(model),
        'feature_names': detect_feature_names(model),
        'import_format': 'keras' if _is_keras_model(model) else 'pkl',
    }


def _make_serializable(value):
    """Convert numpy/non-standard types to JSON-serializable Python types."""
    import math

    if value is None:
        return None
    try:
        import numpy as np
        if isinstance(value, (np.integer,)):
            return int(value)
        if isinstance(value, (np.floating,)):
            f = float(value)
            if math.isnan(f) or math.isinf(f):
                return None
            return f
        if isinstance(value, np.ndarray):
            return value.tolist()
        if isinstance(value, np.bool_):
            return bool(value)
    except ImportError:
        pass
    if isinstance(value, (int, float, str, bool)):
        if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
            return None
        return value
    if isinstance(value, dict):
        return {k: _make_serializable(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        return [_make_serializable(v) for v in value]
    # Fallback: convert to string
    return str(value)
