package de.avatic.lcc.repositories.users;

import de.avatic.lcc.model.db.users.App;
import de.avatic.lcc.model.db.users.Group;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * Repository for managing App entities and their group memberships.
 * <p>
 * This repository uses Spring's JdbcTemplate for database access and provides
 * CRUD-style operations for apps, along with synchronization of app-to-group
 * relations.
 */
@Repository
public class AppRepository {


    private final JdbcTemplate jdbcTemplate;
    private final GroupRepository groupRepository;

    /**
     * Creates a new AppRepository.
     *
     * @param jdbcTemplate    Spring JdbcTemplate used for executing SQL queries
     * @param groupRepository Repository used to resolve group identifiers
     */
    public AppRepository(JdbcTemplate jdbcTemplate, GroupRepository groupRepository) {
        this.jdbcTemplate = jdbcTemplate;
        this.groupRepository = groupRepository;
    }

    /**
     * Lists all apps.
     *
     * @return list of all App records
     */
    @Transactional
    public List<App> listApps() {
        String sql = "SELECT * FROM sys_app";

        return jdbcTemplate.query(sql, new AppMapper());
    }

    /**
     * Retrieves a single App by its primary key.
     *
     * @param id the app identifier
     * @return Optional containing the App when found; otherwise empty
     */
    public Optional<App> getById(Integer id) {
        String sql = "SELECT * FROM sys_app WHERE id = ?";

        var apps = jdbcTemplate.query(sql, new AppMapper(), id);

        if (apps.isEmpty())
            return Optional.empty();

        return Optional.of(apps.getFirst());
    }

    /**
     * Creates a new app when the id is null or updates the existing app otherwise.
     * Also synchronizes the app-to-group mappings based on the provided {@link App#getGroups()}.
     *
     * @param app the app to create or update
     */
    @Transactional
    public Integer update(App app) {

        Integer appId = app.getId();

        List<Integer> groupIds = groupRepository.findGroupIds(app.getGroups().stream().map(Group::getName).toList());

        if (appId == null) {
            String sql = """
                    INSERT INTO sys_app (name, client_id, client_secret)
                    VALUES (?, ?, ?)""";

            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(
                        sql,
                        Statement.RETURN_GENERATED_KEYS
                );
                ps.setString(1, app.getName());
                ps.setString(2, app.getClientId());
                ps.setString(3, app.getClientSecret());
                return ps;
            }, keyHolder);

            appId = Objects.requireNonNull(keyHolder.getKey()).intValue();
        } else {
            String query = """
                    UPDATE sys_app SET name = ? WHERE id = ?""";

            jdbcTemplate.update(query, app.getName(), appId);
        }

        updateAppGroupMappings(appId, groupIds);

        return appId;
    }

    /**
     * Updates the mapping table that associates an app (by id) with a set of groups.
     *
     * @param appId  id of the app whose group relations are to be synchronized
     * @param groups list of group ids that must remain associated with the app
     */
    private void updateAppGroupMappings(Integer appId, List<Integer> groups) {

        // Handle empty groups list case
        if (groups.isEmpty()) {
            jdbcTemplate.update("DELETE FROM sys_app_group_mapping WHERE app_id = ?", appId);
            return;
        } else {
            for (Integer groupId : groups) {
                jdbcTemplate.update(
                        "INSERT IGNORE INTO sys_app_group_mapping (app_id, group_id) VALUES (?, ?)",
                        appId, groupId
                );
            }
        }

        String placeholders = String.join(",", Collections.nCopies(groups.size(), "?"));
        String query = "DELETE FROM sys_app_group_mapping WHERE app_id = ? AND group_id NOT IN (" + placeholders + ")";

        jdbcTemplate.update(
                query,
                ps -> {
                    ps.setInt(1, appId);
                    for (int index = 0; index < groups.size(); index++) {
                        ps.setInt(index + 2, groups.get(index));  // Parameters start at index 2
                    }
                }
        );
    }


    /**
     * Deletes an app by id.
     * Also removes all associated group mappings.
     *
     * @param id id of the app to delete
     */
    @Transactional
    public void delete(Integer id) {
        // First delete all group mappings for this app
        String deleteMappingsSql = "DELETE FROM sys_app_group_mapping WHERE app_id = ?";
        jdbcTemplate.update(deleteMappingsSql, id);

        // Then delete the app itself
        String deleteAppSql = "DELETE FROM sys_app WHERE id = ?";
        jdbcTemplate.update(deleteAppSql, id);
    }

    /**
     * Loads the groups an app belongs to.
     *
     * @param id app id
     * @return list of group domain objects
     */
    private List<Group> getGroupMemberships(int id) {

        String query = "SELECT * FROM sys_group WHERE id IN (SELECT group_id FROM sys_app_group_mapping WHERE app_id = ?)";

        return jdbcTemplate.query(query, (rs, rowNum) -> {
            var group = new Group();

            group.setName(rs.getString("group_name"));
            group.setDescription(rs.getString("group_description"));

            return group;
        }, id);
    }

    public Optional<App> getByClientId(String clientId) {

        String sql = "SELECT * FROM sys_app WHERE client_id = ?";

        var app = jdbcTemplate.query(sql, new AppMapper(), clientId);

        return Optional.ofNullable(app.isEmpty() ? null : app.getFirst());
    }

    /**
     * RowMapper implementation for mapping ResultSet rows to {@link App} objects.
     */
    private class AppMapper implements RowMapper<App> {
        /**
         * Maps a single row of the result set to an App domain object.
         *
         * @param rs     result set positioned at the current row
         * @param rowNum current row number
         * @return mapped App instance
         * @throws SQLException if a JDBC access error occurs
         */
        @Override
        public App mapRow(ResultSet rs, int rowNum) throws SQLException {
            App app = new App();

            app.setId(rs.getInt("id"));
            app.setName(rs.getString("name"));
            app.setClientId(rs.getString("client_id"));
            app.setClientSecret(rs.getString("client_secret"));

            app.setGroups(getGroupMemberships(app.getId()));

            return app;
        }

    }
}
