package com.huangzj.protoplugin.mysql;

import java.sql.Types;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/** 根据列元数据生成带 JPA 注解的实体源码（jakarta.persistence）。 */
public final class JpaEntitySourceGenerator {

    private JpaEntitySourceGenerator() {}

    public static String generate(String packageName, String tableName, List<ColumnMeta> columns,
            Set<String> pkColumns) {
        String className = JavaNames.toClassNameFromTable(tableName);
        List<ColumnMeta> ordered = new ArrayList<>(columns);
        Set<String> pk = pkColumns != null ? pkColumns : Set.of();

        boolean compositePk = pk.size() > 1;
        String pkClassName = className + "Pk";

        StringBuilder imports = new StringBuilder();
        imports.append("import jakarta.persistence.Column;\n");
        imports.append("import jakarta.persistence.Entity;\n");
        imports.append("import jakarta.persistence.Table;\n");
        if (!pk.isEmpty()) {
            imports.append("import jakarta.persistence.Id;\n");
        }
        boolean needGenValue = false;
        if (pk.size() == 1) {
            String only = pk.iterator().next();
            ColumnMeta pkCol = findColumn(ordered, only);
            if (pkCol != null && pkCol.isAutoIncrement()) {
                imports.append("import jakarta.persistence.GeneratedValue;\n");
                imports.append("import jakarta.persistence.GenerationType;\n");
                needGenValue = true;
            }
        }
        if (compositePk) {
            imports.append("import jakarta.persistence.IdClass;\n");
            imports.append("import java.io.Serializable;\n");
            imports.append("import java.util.Objects;\n");
        }

        Set<String> extraImports = new LinkedHashSet<>();
        for (ColumnMeta c : ordered) {
            collectTypeImport(c, extraImports);
        }
        for (String imp : extraImports) {
            imports.append("import ").append(imp).append(";\n");
        }

        StringBuilder sb = new StringBuilder();
        sb.append("package ").append(packageName).append(";\n\n");
        sb.append(imports).append("\n");
        sb.append("/**\n");
        sb.append(" * Generated from MySQL table: ").append(safeJavadoc(tableName)).append("\n");
        sb.append(" */\n");
        sb.append("@Entity\n");
        sb.append("@Table(name = \"").append(escapeJavaString(tableName)).append("\")\n");
        if (compositePk) {
            sb.append("@IdClass(").append(className).append(".").append(pkClassName).append(".class)\n");
        }
        sb.append("public class ").append(className).append(" {\n");
        if (pk.isEmpty()) {
            sb.append("    // TODO: 未从数据库元数据检测到主键，请手工补充 @Id 字段\n");
        }
        sb.append("\n");

        for (ColumnMeta c : ordered) {
            boolean isPk = pk.contains(c.getName());
            String field = JavaNames.toFieldName(c.getName());
            String javaType = mapJavaType(c);
            sb.append("    ");
            if (isPk) {
                sb.append("@Id\n    ");
                if (needGenValue && pk.size() == 1 && pk.contains(c.getName())) {
                    sb.append("@GeneratedValue(strategy = GenerationType.IDENTITY)\n    ");
                }
            }
            sb.append("@Column(name = \"").append(escapeJavaString(c.getName())).append("\"");
            if (!c.isNullable() && !isPk) {
                sb.append(", nullable = false");
            }
            if (c.getColumnSize() > 0 && isStringType(c)) {
                sb.append(", length = ").append(c.getColumnSize());
            }
            if (c.getDecimalDigits() > 0 && (javaType.equals("BigDecimal") || javaType.equals("Double"))) {
                sb.append(", precision = ").append(c.getColumnSize());
                sb.append(", scale = ").append(c.getDecimalDigits());
            }
            sb.append(")\n");
            sb.append("    private ").append(javaType).append(" ").append(field).append(";\n\n");
        }

        // getters / setters
        for (ColumnMeta c : ordered) {
            String field = JavaNames.toFieldName(c.getName());
            String pascal = JavaNames.toPascalCaseField(c.getName());
            String javaType = mapJavaType(c);
            sb.append("    public ").append(javaType).append(" get").append(pascal).append("() {\n");
            sb.append("        return ").append(field).append(";\n");
            sb.append("    }\n\n");
            sb.append("    public void set").append(pascal).append("(").append(javaType).append(" ").append(field)
                    .append(") {\n");
            sb.append("        this.").append(field).append(" = ").append(field).append(";\n");
            sb.append("    }\n\n");
        }

        if (compositePk) {
            sb.append(generatePkClass(pkClassName, ordered, pk));
        }

        sb.append("}\n");
        return sb.toString();
    }

    private static String generatePkClass(String pkClassName, List<ColumnMeta> columns, Set<String> pk) {
        StringBuilder sb = new StringBuilder();
        sb.append("    /**\n");
        sb.append("     * Composite primary key.\n");
        sb.append("     */\n");
        sb.append("    public static class ").append(pkClassName).append(" implements Serializable {\n\n");
        for (ColumnMeta c : columns) {
            if (!pk.contains(c.getName())) {
                continue;
            }
            String field = JavaNames.toFieldName(c.getName());
            String javaType = mapJavaType(c);
            sb.append("        private ").append(javaType).append(" ").append(field).append(";\n");
        }
        sb.append("\n");
        for (ColumnMeta c : columns) {
            if (!pk.contains(c.getName())) {
                continue;
            }
            String field = JavaNames.toFieldName(c.getName());
            String pascal = JavaNames.toPascalCaseField(c.getName());
            String javaType = mapJavaType(c);
            sb.append("        public ").append(javaType).append(" get").append(pascal).append("() {\n");
            sb.append("            return ").append(field).append(";\n");
            sb.append("        }\n\n");
            sb.append("        public void set").append(pascal).append("(").append(javaType).append(" ")
                    .append(field).append(") {\n");
            sb.append("            this.").append(field).append(" = ").append(field).append(";\n");
            sb.append("        }\n\n");
        }
        sb.append("        @Override\n");
        sb.append("        public boolean equals(Object o) {\n");
        sb.append("            if (this == o) return true;\n");
        sb.append("            if (o == null || getClass() != o.getClass()) return false;\n");
        sb.append("            ").append(pkClassName).append(" that = (").append(pkClassName).append(") o;\n");
        sb.append("            return ");
        boolean first = true;
        for (ColumnMeta c : columns) {
            if (!pk.contains(c.getName())) {
                continue;
            }
            String field = JavaNames.toFieldName(c.getName());
            if (!first) {
                sb.append("\n                && ");
            } else {
                first = false;
            }
            sb.append("Objects.equals(").append(field).append(", that.").append(field).append(")");
        }
        if (first) {
            sb.append("true");
        }
        sb.append(";\n");
        sb.append("        }\n\n");
        sb.append("        @Override\n");
        sb.append("        public int hashCode() {\n");
        sb.append("            return Objects.hash(");
        first = true;
        for (ColumnMeta c : columns) {
            if (!pk.contains(c.getName())) {
                continue;
            }
            if (!first) {
                sb.append(", ");
            }
            first = false;
            sb.append(JavaNames.toFieldName(c.getName()));
        }
        sb.append(");\n");
        sb.append("        }\n");
        sb.append("    }\n\n");
        return sb.toString();
    }

    private static ColumnMeta findColumn(List<ColumnMeta> cols, String name) {
        for (ColumnMeta c : cols) {
            if (c.getName().equalsIgnoreCase(name)) {
                return c;
            }
        }
        return null;
    }

    private static void collectTypeImport(ColumnMeta c, Set<String> extraImports) {
        String t = mapJavaType(c);
        switch (t) {
            case "BigDecimal":
                extraImports.add("java.math.BigDecimal");
                break;
            case "LocalDate":
                extraImports.add("java.time.LocalDate");
                break;
            case "LocalTime":
                extraImports.add("java.time.LocalTime");
                break;
            case "LocalDateTime":
                extraImports.add("java.time.LocalDateTime");
                break;
            default:
                break;
        }
    }

    private static boolean isStringType(ColumnMeta c) {
        int st = c.getSqlType();
        return st == Types.CHAR
                || st == Types.VARCHAR
                || st == Types.LONGVARCHAR
                || st == Types.NCHAR
                || st == Types.NVARCHAR
                || st == Types.LONGNVARCHAR;
    }

    private static String mapJavaType(ColumnMeta c) {
        int st = c.getSqlType();
        String tn = c.getTypeName() != null ? c.getTypeName().toLowerCase(Locale.ROOT) : "";
        switch (st) {
            case Types.BIT:
                if (c.getColumnSize() == 1) {
                    return "Boolean";
                }
                return "byte[]";
            case Types.TINYINT:
                if (c.getColumnSize() == 1 && (tn.contains("bool") || tn.contains("bit"))) {
                    return "Boolean";
                }
                return "Integer";
            case Types.SMALLINT:
                return "Integer";
            case Types.INTEGER:
                return "Integer";
            case Types.BIGINT:
                return "Long";
            case Types.FLOAT:
            case Types.REAL:
                return "Float";
            case Types.DOUBLE:
                return "Double";
            case Types.NUMERIC:
            case Types.DECIMAL:
                return "BigDecimal";
            case Types.CHAR:
            case Types.VARCHAR:
            case Types.LONGVARCHAR:
            case Types.NCHAR:
            case Types.NVARCHAR:
            case Types.LONGNVARCHAR:
                return "String";
            case Types.DATE:
                return "LocalDate";
            case Types.TIME:
            case Types.TIME_WITH_TIMEZONE:
                return "LocalTime";
            case Types.TIMESTAMP:
            case Types.TIMESTAMP_WITH_TIMEZONE:
                return "LocalDateTime";
            case Types.BINARY:
            case Types.VARBINARY:
            case Types.LONGVARBINARY:
            case Types.BLOB:
                return "byte[]";
            case Types.BOOLEAN:
                return "Boolean";
            default:
                if (tn.contains("json")) {
                    return "String";
                }
                if (tn.contains("geometry") || tn.contains("blob")) {
                    return "byte[]";
                }
                return "String";
        }
    }

    private static String escapeJavaString(String s) {
        if (s == null) {
            return "";
        }
        return s.replace("\\", "\\\\").replace("\"", "\\\"");
    }

    private static String safeJavadoc(String s) {
        if (s == null) {
            return "";
        }
        return s.replace("*/", "* /");
    }
}
