package com.magistr.app.controller;

import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;

import java.sql.SQLException;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Преобразует технические нарушения целостности БД в безопасные русские HTTP-ошибки.
 */
final class DatabaseConstraintViolationMapper {

    private static final Pattern CONSTRAINT_PATTERN = Pattern.compile(
            "(?i)(?:constraint|ограничени[ея])\\s+[\"']?([a-z0-9_.$-]+)[\"']?"
    );

    private static final Map<String, ViolationResponse> KNOWN_CONSTRAINTS = Map.ofEntries(
            badRequest("chk_teacher_creation_request_status", "Недопустимый статус заявки на преподавателя"),
            badRequest("chk_teacher_department_dates", "Дата окончания назначения кафедры раньше даты начала"),
            badRequest("chk_time_slot_scopes_mode", "Недопустимый режим сетки времени"),
            badRequest("chk_time_slot_scopes_day", "День недели сетки времени должен быть от 1 до 7"),
            badRequest("chk_time_slot_scopes_weekday", "День недели обязателен только для недельной сетки"),
            badRequest("chk_time_slots_order_positive", "Номер пары должен быть положительным"),
            badRequest("chk_time_slots_time_range", "Время начала пары должно быть раньше времени окончания"),
            badRequest("chk_time_slots_duration_positive", "Продолжительность пары должна быть положительной"),
            badRequest("chk_time_slots_duration_matches_range", "Продолжительность пары не соответствует её времени"),
            badRequest("chk_academic_years_dates", "Дата окончания учебного года раньше даты начала"),
            badRequest("chk_semesters_type", "Недопустимый тип семестра"),
            badRequest("chk_semesters_dates", "Дата окончания семестра раньше даты начала"),
            badRequest("chk_academic_calendars_course_count", "Количество курсов должно быть от 1 до 8"),
            badRequest("chk_calendar_days_course_positive", "Номер курса должен быть положительным"),
            badRequest("chk_calendar_days_week_positive", "Номер недели должен быть положительным"),
            badRequest("chk_calendar_days_day", "День недели должен быть от 1 до 7"),
            badRequest("chk_academic_calendar_subjects_semester_positive", "Номер семестра должен быть положительным"),
            badRequest("chk_student_groups_group_size_positive", "Численность группы должна быть больше нуля"),
            badRequest("chk_student_groups_year_start_positive", "Год начала обучения должен быть больше нуля"),
            badRequest("chk_subgroups_student_capacity_positive", "Численность подгруппы должна быть больше нуля"),
            badRequest("chk_student_group_subgroup_capacity", "Сумма численностей активных подгрупп не может превышать численность группы"),
            badRequest("chk_schedule_rules_type_hours_non_negative", "Количество академических часов не может быть отрицательным"),
            badRequest("chk_schedule_rules_has_type_hours", "В правиле должен быть хотя бы один тип занятия с часами"),
            badRequest("chk_schedule_rules_start_weeks_positive", "Неделя начала занятия должна быть положительной"),
            badRequest("chk_schedule_rules_dates", "Период действия правила задан неверно"),
            badRequest("chk_schedule_rules_academic_hours_even", "Количество академических часов должно быть чётным"),
            badRequest("chk_schedule_rule_slots_day", "День недели слота должен быть от 1 до 7"),
            badRequest("chk_schedule_rule_slots_parity", "Недопустимая чётность недели"),
            badRequest("chk_schedule_rule_slots_format", "Формат занятия должен быть «Очно» или «Онлайн»"),
            badRequest("chk_schedule_overrides_action", "Недопустимый тип точечного изменения расписания"),
            badRequest("chk_schedule_overrides_cancel_payload", "Для отмены переданы лишние параметры"),
            badRequest("chk_schedule_overrides_move_payload", "Для переноса переданы неверные параметры"),
            badRequest("chk_schedule_overrides_replace_payload", "Для замены преподавателя переданы неверные параметры"),
            badRequest("chk_schedule_overrides_format", "Недопустимый формат занятия в точечном изменении"),
            conflict("uq_specialty_profiles_name", "Профиль с таким названием уже существует"),
            conflict("uq_subjects_name_ci", "Дисциплина с таким названием уже существует"),
            conflict("uq_time_slot_scopes_default", "Базовая сетка времени уже существует"),
            conflict("uq_time_slot_scopes_weekday", "Сетка времени для этого дня недели уже существует"),
            conflict("uq_time_slots_scope_order", "Пара с таким номером уже существует в выбранной сетке"),
            conflict("uq_semesters_year_type", "Семестр такого типа уже существует в учебном году"),
            conflict("uq_academic_calendar_title", "Календарный график с таким названием уже существует"),
            conflict("uq_calendar_days", "День календарного графика уже существует"),
            conflict("uq_academic_calendar_subject", "Дисциплина уже добавлена в этот семестр графика"),
            conflict("uq_group_calendar_year", "Группе уже назначен календарный график на учебный год"),
            conflict("uq_schedule_rule_slots_exact_payload", "Такой слот правила расписания уже существует"),
            conflict("uq_schedule_overrides_slot_date", "Для этой пары и даты уже существует точечное изменение"),
            conflict("ux_subgroups_active_group_name", "Активная подгруппа с таким названием уже существует"),
            conflict("ex_time_slots_scope_no_overlap", "Интервал пары пересекается с другой парой выбранной сетки"),
            conflict("ex_academic_years_no_overlap", "Период учебного года пересекается с существующим"),
            conflict("ex_semesters_year_no_overlap", "Период семестра пересекается с существующим"),
            conflict("ex_teacher_primary_department_no_overlap", "Период основной кафедры пересекается с другим назначением")
    );

    ViolationResponse map(DataIntegrityViolationException exception) {
        String constraintName = constraintName(exception).orElse(null);
        String sqlState = sqlState(exception).orElse(null);
        if (constraintName != null) {
            ViolationResponse known = KNOWN_CONSTRAINTS.get(constraintName);
            if (known != null) {
                return known.withDetails(constraintName, sqlState);
            }
            if (constraintName.endsWith("_fkey")) {
                return new ViolationResponse(
                        HttpStatus.CONFLICT,
                        "Операция невозможна из-за связанных данных",
                        constraintName,
                        sqlState
                );
            }
        }

        if ("23502".equals(sqlState)) {
            return new ViolationResponse(
                    HttpStatus.BAD_REQUEST,
                    "Не заполнено обязательное поле",
                    constraintName,
                    sqlState
            );
        }
        if ("23514".equals(sqlState)) {
            return new ViolationResponse(
                    HttpStatus.BAD_REQUEST,
                    "Данные нарушают правила предметной области",
                    constraintName,
                    sqlState
            );
        }
        if ("23505".equals(sqlState)) {
            return new ViolationResponse(
                    HttpStatus.CONFLICT,
                    "Такая запись уже существует",
                    constraintName,
                    sqlState
            );
        }
        if ("23503".equals(sqlState) || "23P01".equalsIgnoreCase(sqlState)) {
            return new ViolationResponse(
                    HttpStatus.CONFLICT,
                    "Операция невозможна из-за конфликта или связанных данных",
                    constraintName,
                    sqlState
            );
        }
        return new ViolationResponse(
                HttpStatus.CONFLICT,
                "Операция нарушает ограничения целостности данных",
                constraintName,
                sqlState
        );
    }

    private Optional<String> constraintName(Throwable failure) {
        for (Throwable current = failure; current != null; current = nextCause(current)) {
            if (current instanceof ConstraintViolationException constraintViolation
                    && constraintViolation.getConstraintName() != null) {
                return normalize(constraintViolation.getConstraintName());
            }
            String message = current.getMessage();
            if (message != null) {
                Matcher matcher = CONSTRAINT_PATTERN.matcher(message);
                if (matcher.find()) {
                    return normalize(matcher.group(1));
                }
            }
        }
        return Optional.empty();
    }

    private Optional<String> sqlState(Throwable failure) {
        for (Throwable current = failure; current != null; current = nextCause(current)) {
            if (current instanceof SQLException sqlException && sqlException.getSQLState() != null) {
                return Optional.of(sqlException.getSQLState());
            }
        }
        return Optional.empty();
    }

    private Throwable nextCause(Throwable current) {
        Throwable cause = current.getCause();
        return cause == current ? null : cause;
    }

    private Optional<String> normalize(String constraintName) {
        if (constraintName == null || constraintName.isBlank()) {
            return Optional.empty();
        }
        String normalized = constraintName.trim()
                .replace("\"", "")
                .toLowerCase(Locale.ROOT);
        int dot = normalized.lastIndexOf('.');
        return Optional.of(dot >= 0 ? normalized.substring(dot + 1) : normalized);
    }

    private static Map.Entry<String, ViolationResponse> badRequest(String name, String message) {
        return Map.entry(name, new ViolationResponse(HttpStatus.BAD_REQUEST, message, name, "23514"));
    }

    private static Map.Entry<String, ViolationResponse> conflict(String name, String message) {
        return Map.entry(name, new ViolationResponse(HttpStatus.CONFLICT, message, name, null));
    }

    record ViolationResponse(HttpStatus status,
                             String message,
                             String constraintName,
                             String sqlState) {

        private ViolationResponse withDetails(String actualConstraintName, String actualSqlState) {
            return new ViolationResponse(status, message, actualConstraintName, actualSqlState);
        }
    }
}
