from __future__ import annotations

import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

RANDOM_STATE = 42
TEST_SIZE = 0.25
HARD_MARGIN_APPROXIMATION_C = 1_000_000.0

FEATURE_COLUMNS = [
    "surface_defect_score",
    "dimension_deviation",
]

ACCEPT_LABEL = -1
REJECT_LABEL = 1


def create_dataset() -> tuple[pd.DataFrame, pd.Series]:
    """Create a small, linearly separable quality-control dataset."""
    rows = [
        # Acceptable parts (-1)
        (0.0, 0.0, ACCEPT_LABEL),
        (1.0, 0.0, ACCEPT_LABEL),
        (0.0, 1.0, ACCEPT_LABEL),
        (0.4, 0.4, ACCEPT_LABEL),
        (0.8, 0.2, ACCEPT_LABEL),
        (0.2, 0.8, ACCEPT_LABEL),
        (1.1, 0.3, ACCEPT_LABEL),
        (0.3, 1.1, ACCEPT_LABEL),
        (0.8, 0.8, ACCEPT_LABEL),
        (1.2, 0.5, ACCEPT_LABEL),
        (0.5, 1.2, ACCEPT_LABEL),
        (1.0, 1.0, ACCEPT_LABEL),
        # Reject parts (+1)
        (3.0, 2.2, REJECT_LABEL),
        (3.2, 2.4, REJECT_LABEL),
        (3.5, 2.0, REJECT_LABEL),
        (3.0, 3.0, REJECT_LABEL),
        (4.0, 2.2, REJECT_LABEL),
        (3.8, 3.0, REJECT_LABEL),
        (4.2, 2.8, REJECT_LABEL),
        (3.6, 3.6, REJECT_LABEL),
        (4.5, 3.2, REJECT_LABEL),
        (3.2, 3.8, REJECT_LABEL),
        (4.0, 4.0, REJECT_LABEL),
        (4.6, 3.8, REJECT_LABEL),
    ]

    frame = pd.DataFrame(
        rows,
        columns=[*FEATURE_COLUMNS, "quality_label"],
    )
    X = frame[FEATURE_COLUMNS].copy()
    y = frame["quality_label"].copy()
    return X, y


def build_model() -> Pipeline:
    """Build a leakage-safe scaling and linear-SVM pipeline."""
    return Pipeline(
        steps=[
            ("scaler", StandardScaler()),
            (
                "classifier",
                SVC(
                    kernel="linear",
                    C=HARD_MARGIN_APPROXIMATION_C,
                    probability=False,
                ),
            ),
        ]
    )


def signed_geometric_distance(
    model: Pipeline,
    X: pd.DataFrame,
) -> np.ndarray:
    """Return signed distance to the learned boundary in scaled space."""
    scaler: StandardScaler = model.named_steps["scaler"]
    classifier: SVC = model.named_steps["classifier"]

    X_scaled = scaler.transform(X)
    raw_scores = classifier.decision_function(X_scaled)
    weight_norm = np.linalg.norm(classifier.coef_.ravel())

    if weight_norm == 0:
        raise ValueError("The learned weight vector has zero norm.")

    return raw_scores / weight_norm


def main() -> None:
    X, y = create_dataset()

    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=TEST_SIZE,
        random_state=RANDOM_STATE,
        stratify=y,
    )

    model = build_model()
    model.fit(X_train, y_train)

    train_predictions = model.predict(X_train)
    test_predictions = model.predict(X_test)

    print(
        "Training accuracy:",
        f"{accuracy_score(y_train, train_predictions):.3f}",
    )
    print(
        "Test accuracy:",
        f"{accuracy_score(y_test, test_predictions):.3f}",
    )

    classifier: SVC = model.named_steps["classifier"]
    weight_norm = np.linalg.norm(classifier.coef_.ravel())
    margin_to_boundary = 1.0 / weight_norm
    full_margin_width = 2.0 / weight_norm

    print("Number of support vectors:", classifier.n_support_.sum())
    print("Margin to nearest support hyperplane:", f"{margin_to_boundary:.3f}")
    print("Full margin width:", f"{full_margin_width:.3f}")

    support_positions = classifier.support_
    support_rows = X_train.iloc[support_positions].copy()
    support_rows["label"] = y_train.iloc[support_positions].to_numpy()

    print("\nSupport-vector training rows:")
    print(support_rows.to_string(index=False))

    new_part = pd.DataFrame(
        [
            {
                "surface_defect_score": 3.4,
                "dimension_deviation": 2.5,
            }
        ]
    )

    raw_score = float(model.decision_function(new_part)[0])
    distance = float(signed_geometric_distance(model, new_part)[0])
    prediction = int(model.predict(new_part)[0])

    print("\nNew part:")
    print(f"Raw decision score: {raw_score:.3f}")
    print(f"Signed geometric distance: {distance:.3f}")
    print(
        "Prediction:",
        "Reject" if prediction == REJECT_LABEL else "Accept",
    )


if __name__ == "__main__":
    main()
