import numpy as np
import torch
import torch.nn as nn
import argparse
from torch.utils.data import random_split, DataLoader, TensorDataset
from torch.optim.lr_scheduler import ReduceLROnPlateau
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import matplotlib.pyplot as plt

parser = argparse.ArgumentParser(
    prog="train_nn"
)
parser.add_argument("-g", "--graph", action="store_true", default=False, help="Graph losses")
parser.add_argument("-s", "--save", default="", help="Graph save location")
args = parser.parse_args()
graph = args.graph
save = args.save


class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()

        self.features = nn.Sequential(
            nn.Linear(8, 512),
            nn.ReLU(),
            nn.BatchNorm1d(512),
            nn.Dropout(p=0.4),

            nn.Linear(512, 256),
            nn.ReLU(),
            nn.BatchNorm1d(256),
            nn.Dropout(p=0.4),

            nn.Linear(256, 128),
            nn.ReLU(),
            nn.BatchNorm1d(128),
            nn.Dropout(p=0.4),

            nn.Linear(128, 64),
            nn.ReLU(),
            nn.BatchNorm1d(64),
            nn.Dropout(p=0.4),

            nn.Linear(64, 4)
        )

    def forward(self, x):
        return self.features(x)


data = np.load("clean.npy")
data_x = np.append(data[:, 0:2], data[:, 6:12], axis=1)  # grade, sex, ses, occupation, living, commute, sleep, absence
data_y = data[:, 2:6]  # gpa, math, slovak, english
print(f"Loaded data of shape {data.shape}")
print(f"\tx: {data_x.shape}")
print(f"\ty: {data_y.shape}")
print("")

train_size = int(len(data) * 0.8)
test_size = len(data) - train_size
torch.manual_seed(42)

batch_size = 32
epochs = 200
lr = 0.001
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

dataset = TensorDataset(torch.Tensor(data_x), torch.Tensor(data_y))
train_dataset, test_dataset = random_split(dataset, [train_size, test_size])
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

print(f"Initialized dataloaders")
print(f"\ttrain: {len(train_dataloader)} batches ({len(train_dataset)} samples)")
print(f"\ttest: {len(test_dataloader)} batches ({len(test_dataset)} samples)")
print("")

model = NeuralNetwork()
model.to(device)

print(f"Initialized model")
print(model)
print("")

loss_fn = nn.MSELoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)
scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5)

train_losses = []
test_losses = []

for epoch in range(epochs):
    print(f"Epoch {epoch + 1}/{epochs}...\r", end="")

    model.train()
    train_loss = 0.0

    for X, y in train_dataloader:
        X, y = X.to(device), y.to(device)
        optimizer.zero_grad()

        pred = model(X)
        loss = loss_fn(pred, y)

        loss.backward()
        optimizer.step()

        train_loss += loss.item() * X.size(0)

    train_loss /= len(train_dataset)
    train_losses.append(train_loss)

    model.eval()
    test_loss = 0.0

    with torch.no_grad():
        for X, y in test_dataloader:
            X, y = X.to(device), y.to(device)

            pred = model(X)
            loss = loss_fn(pred, y)

            test_loss += loss.item() * X.size(0)

    test_loss /= len(test_dataset)
    test_losses.append(test_loss)

    print(
        f"Epoch {epoch + 1}/{epochs}\n"
        f"Train loss: {train_loss:.4f}\n"
        f"Test loss: {test_loss:.4f}\n"
    )
    scheduler.step(test_loss)

print(f"Average train loss: {sum(train_losses) / len(train_losses):.4f}")
print(f"Average test loss: {sum(test_losses) / len(test_losses):.4f}")

torch.save(model.state_dict(), "model.pth")
print("Model saved to model.pth")

model.eval()
all_preds = []
all_labels = []

with torch.no_grad():
    for X, y in test_dataloader:
        X, y = X.to(device), y.to(device)
        pred = model(X)
        all_preds.append(pred.argmax(dim=1).cpu().numpy())
        all_labels.append(y.argmax(dim=1).cpu().numpy())

all_preds = np.concatenate(all_preds)
all_labels = np.concatenate(all_labels)

accuracy = accuracy_score(all_labels, all_preds)
precision = precision_score(all_labels, all_preds, average='weighted', zero_division=0)
recall = recall_score(all_labels, all_preds, average='weighted', zero_division=0)
f1 = f1_score(all_labels, all_preds, average='weighted')

print("\nEvaluation Metrics:")
print(f"Accuracy:  {accuracy:.4f}")
print(f"Precision: {precision:.4f}")
print(f"Recall:    {recall:.4f}")
print(f"F1 Score:  {f1:.4f}")

if graph:
    x = np.arange(1, epochs + 1, 1)

    plt.figure(figsize=(8, 6))
    plt.plot(x, train_losses, color="red", label="Strata trénovania")
    plt.plot(x, test_losses, color="blue", label="Strata testovania")

    plt.xlabel("Epocha", fontweight="bold", fontsize=14)
    plt.ylabel("Strata", fontweight="bold", fontsize=14)
    plt.title("Priebeh trénovania", fontsize=20)

    plt.text(0.99, 0.99,
             f"Presnosť: {accuracy:.4f}\nPrecíznosť: {precision:.4f}\nOdvolanie: {recall:.4f}\nF1 skóre: {f1:.4f}",
             ha="right", va="top", transform=plt.gca().transAxes, fontweight="bold", fontsize=14)

    plt.legend()
    plt.tight_layout()
    if save != "":
        plt.savefig(save)
    else:
        plt.show()
