import argparse
import csv
import json
import math
import os
import random
import joblib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import lightgbm as lgb

from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error

# =========================================================
# Hyper Parameters
# =========================================================

MAX_NOTES = 2000
FACTOR_COUNT = 4
INPUT_DIM = MAX_NOTES * FACTOR_COUNT

TRAIN_SIZE = 0
VALID_SIZE = 0
RANDOM_STATE = 42

PARAMS = {
    'objective': 'regression',
    'metric': 'mae',
    'verbosity': -1,
    'boosting_type': 'gbdt',
    'random_state': RANDOM_STATE,
    'learning_rate': 0.02,
    'num_leaves': 63,
    'n_estimators': 2000
}

CONTINUE_TRAINING = True
ERROR_TOLERANCE = 0.1

# =========================================================
# 파일명
# =========================================================

FACTORS_FILENAME = "factors.json"
MEASURE_FILENAME = "measure.csv"
MODEL_FILENAME = "model.pkl"
SCALER_FILENAME = "scaler.pkl"

def safe_float(value):
    if value is None: return 0.0
    x = float(value)
    return x if math.isfinite(x) else 0.0

def train_model(working_dir: str, data_dir: str):
    random.seed(RANDOM_STATE)

    factors_path = os.path.join(working_dir, FACTORS_FILENAME)
    measure_path = os.path.join(data_dir, MEASURE_FILENAME)
    model_path = os.path.join(working_dir, MODEL_FILENAME)
    scaler_path = os.path.join(working_dir, SCALER_FILENAME)

    with open(factors_path, "r", encoding="utf-8") as f:
        factor_data = json.load(f)

    feature_map = {(str(item["songno"]), str(item["difficulty"])): item["factors"] for item in factor_data}

    dataset = []
    with open(measure_path, "r", encoding="utf-8") as f:
        reader = csv.reader(f)
        next(reader, None)
        for row in reader:
            if len(row) < 3: continue
            measure, songno, diff = safe_float(row[0]), str(row[1]), str(row[2])
            key = (songno, diff)
            
            if key in feature_map:
                raw_factors = feature_map[key]
                vector = np.zeros(INPUT_DIM, dtype=np.float32)
                for i in range(min(len(raw_factors), MAX_NOTES)):
                    for j in range(FACTOR_COUNT):
                        vector[i * FACTOR_COUNT + j] = safe_float(raw_factors[i][j])
                dataset.append((vector, measure, songno, diff))

    random.shuffle(dataset)
    if len(dataset) < (TRAIN_SIZE + VALID_SIZE):
        raise ValueError(f"Dataset size {len(dataset)} < required {TRAIN_SIZE + VALID_SIZE}")

    train_ds = dataset[:TRAIN_SIZE]
    valid_ds = dataset[TRAIN_SIZE:TRAIN_SIZE + VALID_SIZE]

    X_train = np.array([x for x, _, _, _ in train_ds])
    y_train = np.array([y for _, y, _, _ in train_ds])
    X_valid = np.array([x for x, _, _, _ in valid_ds])
    y_valid = np.array([y for _, y, _, _ in valid_ds])
    valid_info = [(s, d) for _, _, s, d in valid_ds]

    if CONTINUE_TRAINING and os.path.exists(scaler_path):
        scaler = joblib.load(scaler_path)
    else:
        scaler = StandardScaler()
        scaler.fit(X_train)

    X_train = scaler.transform(X_train)
    X_valid = scaler.transform(X_valid)

    if CONTINUE_TRAINING and os.path.exists(model_path):
        model = joblib.load(model_path)
        model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], init_model=model, callbacks=[lgb.early_stopping(stopping_rounds=100)])
    else:
        model = lgb.LGBMRegressor(**PARAMS)
        model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], callbacks=[lgb.early_stopping(stopping_rounds=100)])

    pred = model.predict(X_valid)
    mae = mean_absolute_error(y_valid, pred)
    accuracy = np.sum(np.abs(pred - y_valid) <= ERROR_TOLERANCE) / len(y_valid)
    print(f"MAE: {mae:.4f} | Accuracy: {accuracy:.4f}")

    # Results save
    validate_details = []
    for i in range(len(y_valid)):
        validate_details.append({"songno": valid_info[i][0], "diff": valid_info[i][1], "actual": float(y_valid[i]), "predicted": float(pred[i]), "error": float(y_valid[i] - pred[i])})
    
    validate_details.sort(key=lambda x: abs(x["error"]), reverse=True)
    with open(os.path.join(working_dir, "validate.json"), "w", encoding="utf-8") as f:
        json.dump({"summary": {"mae": float(mae), "accuracy": float(accuracy)}, "details": validate_details}, f, indent=2)

    # Plot
    plt.switch_backend('Agg')
    df_plot = pd.DataFrame(validate_details)
    df_plot['abs_error'] = df_plot['error'].abs()
    df_plot = df_plot.sort_values('abs_error', ascending=False).reset_index(drop=True)
    plt.figure(figsize=(12, 6))
    sns.scatterplot(data=df_plot, x=df_plot.index, y='abs_error', color='teal')
    plt.axhline(0.2, color='green', linestyle='--')
    plt.ylim(0, 4)
    plt.savefig(os.path.join(working_dir, "validate.png"))
    
    joblib.dump(model, model_path)
    joblib.dump(scaler, scaler_path)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--workingDir", required=True)
    parser.add_argument("--dataDir", required=True)
    parser.add_argument("--trainSize", required=True, type=int)
    parser.add_argument("--validSize", required=True, type=int)
    args = parser.parse_args()
    TRAIN_SIZE, VALID_SIZE = args.trainSize, args.validSize
    train_model(args.workingDir, args.dataDir)
