import os
from typing import Callable
import math

import numpy as np
from bson import ObjectId
import torch
from torch import nn

from utils.data_utils import get_collated_batch_for_key


def simple_get_y(collated_batch):
    """
    Get y from collated batch
    Args:
        collated_batch: collated batch to get y from

    Returns:
        np.ndarray: y
    """
    return collated_batch[1]


def simple_model_save(model: nn.Module,
                      training_configuration: dict) -> None:
    """
    Save model to disk
    Args:
        model: model to save
        training_configuration: training configuration
    Returns:
        None
    """
    model_path = os.path.join(training_configuration["training_dir"], "model.pt")
    torch.save(model.state_dict(), model_path)


def simple_model_load(model_configuration: dict,
                      training_configuration: dict,
                      sample_key: str | ObjectId,
                      device: str,
                      model_creation_fn: Callable,
                      *args, **kwargs) -> nn.Module:
    """
    Load model from disk
    Args:
        model_configuration: model configuration
        training_configuration: training configuration
        sample_key: sample key to get sample data batch with
        device: device to load model on
        model_creation_fn: function to create model
    Returns:
        model: loaded model
    """
    training_dir = training_configuration["training_dir"]
    model_state_path = os.path.join(training_dir, "model.pt")
    model = model_creation_fn(model_configuration,
                              sample_key, *args, **kwargs)
    model.to(device)
    model.load_state_dict(torch.load(model_state_path))
    return model


def simple_model_creation(model_configuration: dict,
                          sample_key: str | ObjectId,
                          lmdb_env=None) -> nn.Module:
    """
    Create model from configuration
    Args:
        model_configuration: model configuration
        sample_key: sample key to get sample data batch with
        lmdb_env: LMDB environment to use for getting sample data batch

    Returns:
        model: created model
    """
    sample_item = get_collated_batch_for_key(sample_key, model_configuration, lmdb_env=lmdb_env)
    input_size = sample_item[0].shape[2]
    output_size = sample_item[1].shape[2]
    model_class = model_configuration["model_class"]
    cnn_model = model_class(input_dim=input_size,
                            output_dim=output_size,
                            **model_configuration["model_parameters"], )
    return cnn_model


def simple_x_y_predict(model: nn.Module,
                       collated_batch: tuple[torch.Tensor, torch.Tensor],
                       device: str,
                       batch_size: int,
                       *args, **kwargs) -> torch.Tensor:
    """
    Predict y from x
    Args:
        model: model to use for prediction, must have 'predict' method
        collated_batch: collated data batch to predict with,
            must be a tuple of (x, y) where x is the input data and y is the target data
        device: device to use for prediction
        *args: additional arguments to pass to the model's predict method
        **kwargs: additional keyword arguments to pass to the model's predict method

    Returns:
        torch.Tensor: predicted y as numpy array on CPU
    """

    model.eval()
    x = collated_batch[0].to(device).float()
    batches = list()
    num_batches = math.ceil(len(x) / batch_size)
    for i in range(num_batches):
        start = i * batch_size
        end = (i + 1) * batch_size
        if end > len(x):
            end = len(x)
        batch_slice = x[start:end]
        batches.append(batch_slice)

    preds = list()
    with torch.no_grad():
        for batch in batches:
            batch = batch.to(device).float()
            pred = model(batch, *args, **kwargs)
            preds.append(pred.cpu().numpy())
    return np.concatenate(preds)


def simple_get_y(collated_batch: tuple[torch.Tensor, torch.Tensor], ) -> torch.Tensor:
    """
    Get y from collated batch
    Args:
        collated_batch: collated batch to get y from

    Returns:
        torch.Tensor: y
    """
    return collated_batch[1]
