package squirrel;

import java.sql.*;
import java.util.*;

public abstract class Model {
    protected static Connection conn;

    protected String tableName;
    protected Set<String> columns = new HashSet<>();
    protected Map<String, Object> attributes = new LinkedHashMap<>();

    protected boolean rowMode = true;

    // Only one classmode instance per subclass
    private static final Map<Class<? extends Model>, Model> classModeInstances = new HashMap<>();

    public Model() {
        if (conn == null) {
            conn = Database.getConnection();
        }
        // Always rowMode by default
        this.rowMode = true;
    }

    // Call once per subclass to create the classmode instance
    public static <T extends Model> T initializeClassMode(Class<T> clazz) {
        synchronized (classModeInstances) {
            if (classModeInstances.containsKey(clazz)) {
                throw new IllegalStateException("Classmode instance already initialized for " + clazz.getSimpleName());
            }
            try {
                T instance = clazz.getDeclaredConstructor().newInstance();
                instance.rowMode = false;
                classModeInstances.put(clazz, instance);
                return instance;
            } catch (Exception e) {
                throw new RuntimeException("Failed to initialize classmode instance for " + clazz.getSimpleName(), e);
            }
        }
    }

    // Get the classmode instance for a subclass
    public static <T extends Model> T getClassMode(Class<T> clazz) {
        Model instance = classModeInstances.get(clazz);
        if (instance == null) {
            throw new IllegalStateException("Classmode instance not initialized for " + clazz.getSimpleName());
        }
        return clazz.cast(instance);
    }

    // -------------------------------
    // Query (class/global mode only)
    // -------------------------------
    public <T extends Model> List<T> all() {
        ensureClassMode("all()");
        String sql = "SELECT * FROM " + getTableName();
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            ResultSet rs = stmt.executeQuery();
            List<T> results = new ArrayList<>();
            while (rs.next()) {
                @SuppressWarnings("unchecked")
                T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
                instance.seedFromRow(rs);
                instance.rowMode = true;
                results.add(instance);
            }
            rs.close();
            return results;
        } catch (Exception e) {
            throw new RuntimeException("Failed to execute all() for model '" + getTableName() + "'", e);
        }
    }

    public <T extends Model> T find(int id) {
        ensureClassMode("find()");
        String sql = "SELECT * FROM " + getTableName() + " WHERE id = ?";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                @SuppressWarnings("unchecked")
                T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
                instance.seedFromRow(rs);
                instance.rowMode = true;
                rs.close();
                return instance;
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to execute find() for model '" + getTableName() + "'", e);
        }
        return null;
    }

    public <T extends Model> List<T> where(Map<String, Object> filters) {
        ensureClassMode("where()");
        StringBuilder where = new StringBuilder("1=1");
        List<Object> values = new ArrayList<>();
        for (var entry : filters.entrySet()) {
            where.append(" AND ").append(entry.getKey()).append(" = ?");
            values.add(entry.getValue());
        }

        String sql = "SELECT * FROM " + getTableName() + " WHERE " + where;
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            for (int i = 0; i < values.size(); i++) {
                stmt.setObject(i + 1, values.get(i));
            }
            ResultSet rs = stmt.executeQuery();
            List<T> results = new ArrayList<>();
            while (rs.next()) {
                @SuppressWarnings("unchecked")
                T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
                instance.seedFromRow(rs);
                instance.rowMode = true;
                results.add(instance);
            }
            rs.close();
            return results;
        } catch (Exception e) {
            throw new RuntimeException("Failed to execute where() for model '" + getTableName() + "'", e);
        }
    }

    // -------------------------------
    // Row-only actions
    // -------------------------------
    public boolean create() {
        ensureRowMode("create()");
        validateColumns(attributes.keySet());
        StringBuilder cols = new StringBuilder();
        StringBuilder vals = new StringBuilder();
        List<Object> params = new ArrayList<>();
        for (String col : attributes.keySet()) {
            if (col.equals("id")) continue;
            if (cols.length() > 0) {
                cols.append(", ");
                vals.append(", ");
            }
            cols.append(col);
            vals.append("?");
            params.add(attributes.get(col));
        }
        String sql = "INSERT INTO " + getTableName() + " (" + cols + ") VALUES (" + vals + ")";
        try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            for (int i = 0; i < params.size(); i++) {
                stmt.setObject(i + 1, params.get(i));
            }
            int affected = stmt.executeUpdate();
            if (affected == 0) {
                throw new RuntimeException("Failed to insert row for model '" + getTableName() + "'");
            }
            ResultSet keys = stmt.getGeneratedKeys();
            if (keys.next()) {
                set("id", keys.getInt(1));
            }
            keys.close();
            return true;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to execute create() for model '" + getTableName() + "'", e);
        }
    }

    public boolean save() {
        ensureRowMode("save()");
        if (!attributes.containsKey("id")) {
            throw new IllegalStateException("No id set for row instance");
        }
        int id = (int) attributes.get("id");

        StringBuilder set = new StringBuilder();
        List<Object> values = new ArrayList<>();

        for (var entry : attributes.entrySet()) {
            if (entry.getKey().equals("id")) continue;
            if (set.length() > 0) set.append(", ");
            set.append(entry.getKey()).append("=?");
            values.add(entry.getValue());
        }

        String sql = "UPDATE " + getTableName() + " SET " + set + " WHERE id=?";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            for (int i = 0; i < values.size(); i++) {
                stmt.setObject(i + 1, values.get(i));
            }
            stmt.setInt(values.size() + 1, id);
            return stmt.executeUpdate() > 0;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to execute save() for model '" + getTableName() + "'", e);
        }
    }

    public boolean delete() {
        ensureRowMode("delete()");
        if (!attributes.containsKey("id")) {
            throw new IllegalStateException("No id set for row instance");
        }
        int id = (int) attributes.get("id");
        String sql = "DELETE FROM " + getTableName() + " WHERE id = ?";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setInt(1, id);
            return stmt.executeUpdate() > 0;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to execute delete() for model '" + getTableName() + "'", e);
        }
    }

    public Object get(String column) {
        ensureRowMode("get()");
        return attributes.get(column);
    }

    public void set(String column, Object value) {
        ensureRowMode("set()");
        if (!columns.contains(column)) {
            throw new IllegalArgumentException("Unknown column '" + column + "' for model '" + getTableName() + "'");
        }
        attributes.put(column, value);
    }

    // -------------------------------
    // Helpers
    // -------------------------------
    protected void seedFromRow(ResultSet rs) {
        try {
            attributes = new LinkedHashMap<>();
            ResultSetMetaData meta = rs.getMetaData();
            for (int i = 1; i <= meta.getColumnCount(); i++) {
                attributes.put(meta.getColumnName(i), rs.getObject(i));
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to seed from row for model '" + getTableName() + "'", e);
        }
    }

    protected void validateColumns(Collection<String> keys) {
        for (String key : keys) {
            if (!columns.contains(key)) {
                throw new IllegalArgumentException(
                    "Unknown column '" + key + "' for model '" + getTableName() + "'"
                );
            }
        }
    }

    protected String getTableName() {
        if (this.tableName != null && !this.tableName.isEmpty()) {
            return this.tableName;
        }
        String className = this.getClass().getSimpleName();
        return className.toLowerCase() + "s";
    }

    private void ensureRowMode(String action) {
        if (!rowMode) {
            throw new IllegalStateException(action + " requires a row-bound instance");
        }
    }

    private void ensureClassMode(String action) {
        if (rowMode) {
            throw new IllegalStateException(action + " requires a class/global instance");
        }
    }

    // -------------------------------
    // toString
    // -------------------------------
    @Override
    public String toString() {
        if (!rowMode) {
            return "[Model: " + getClass().getSimpleName() + " | table=" + getTableName() + "]";
        }
        StringBuilder sb = new StringBuilder(getClass().getSimpleName() + " { ");
        if (attributes.containsKey("id")) {
            sb.append("id: ").append(attributes.get("id")).append(", ");
        }
        for (var entry : attributes.entrySet()) {
            if (entry.getKey().equals("id")) continue;
            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(", ");
        }
        if (!attributes.isEmpty()) sb.setLength(sb.length() - 2);
        sb.append(" }");
        return sb.toString();
    }
}
