package com.cameleer.server.app.rbac;

import com.cameleer.server.core.rbac.GroupRepository;
import com.cameleer.server.core.rbac.GroupSummary;
import com.cameleer.server.core.rbac.RbacService;
import com.cameleer.server.core.rbac.RbacStats;
import com.cameleer.server.core.rbac.RoleSummary;
import com.cameleer.server.core.rbac.SystemRole;
import com.cameleer.server.core.rbac.UserDetail;
import com.cameleer.server.core.rbac.UserSummary;
import com.cameleer.server.core.security.UserInfo;
import com.cameleer.server.core.security.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.*;

@Service
public class RbacServiceImpl implements RbacService {

    private final JdbcTemplate jdbc;
    private final UserRepository userRepository;
    private final GroupRepository groupRepository;

    public RbacServiceImpl(JdbcTemplate jdbc, UserRepository userRepository,
                           GroupRepository groupRepository) {
        this.jdbc = jdbc;
        this.userRepository = userRepository;
        this.groupRepository = groupRepository;
    }

    @Override
    public List<UserDetail> listUsers() {
        return userRepository.findAll().stream()
                .map(this::buildUserDetail)
                .toList();
    }

    @Override
    public UserDetail getUser(String userId) {
        UserInfo user = userRepository.findById(userId).orElse(null);
        if (user == null) return null;
        return buildUserDetail(user);
    }

    private UserDetail buildUserDetail(UserInfo user) {
        List<RoleSummary> directRoles = getDirectRolesForUser(user.userId());
        List<GroupSummary> directGroups = getDirectGroupsForUser(user.userId());
        List<RoleSummary> effectiveRoles = getEffectiveRolesForUser(user.userId());
        List<GroupSummary> effectiveGroups = getEffectiveGroupsForUser(user.userId());
        return new UserDetail(user.userId(), user.provider(), user.email(),
                user.displayName(), user.createdAt(),
                directRoles, directGroups, effectiveRoles, effectiveGroups);
    }

    @Override
    public void assignRoleToUser(String userId, UUID roleId) {
        jdbc.update("""
                INSERT INTO user_roles (user_id, role_id, origin)
                VALUES (?, ?, 'direct')
                ON CONFLICT (user_id, role_id, origin) DO NOTHING
                """, userId, roleId);
    }

    @Override
    public void removeRoleFromUser(String userId, UUID roleId) {
        if (SystemRole.ADMIN_ID.equals(roleId) && getEffectivePrincipalsForRole(SystemRole.ADMIN_ID).size() <= 1) {
            throw new ResponseStatusException(HttpStatus.CONFLICT,
                    "Cannot remove the ADMIN role: at least one admin user must exist");
        }
        jdbc.update("DELETE FROM user_roles WHERE user_id = ? AND role_id = ?", userId, roleId);
    }

    @Override
    public void addUserToGroup(String userId, UUID groupId) {
        jdbc.update("""
                INSERT INTO user_groups (user_id, group_id, origin)
                VALUES (?, ?, 'direct')
                ON CONFLICT (user_id, group_id, origin) DO NOTHING
                """, userId, groupId);
    }

    @Override
    public void removeUserFromGroup(String userId, UUID groupId) {
        jdbc.update("DELETE FROM user_groups WHERE user_id = ? AND group_id = ?", userId, groupId);
    }

    @Override
    public List<RoleSummary> getEffectiveRolesForUser(String userId) {
        List<RoleSummary> direct = getDirectRolesForUser(userId);

        List<GroupSummary> effectiveGroups = getEffectiveGroupsForUser(userId);
        Map<UUID, RoleSummary> roleMap = new LinkedHashMap<>();
        for (RoleSummary r : direct) {
            roleMap.put(r.id(), r);
        }
        for (GroupSummary group : effectiveGroups) {
            List<RoleSummary> groupRoles = jdbc.query("""
                    SELECT r.id, r.name, r.system FROM group_roles gr
                    JOIN roles r ON r.id = gr.role_id WHERE gr.group_id = ?
                    """, (rs, rowNum) -> new RoleSummary(
                    rs.getObject("id", UUID.class),
                    rs.getString("name"),
                    rs.getBoolean("system"),
                    group.name()
            ), group.id());
            for (RoleSummary r : groupRoles) {
                roleMap.putIfAbsent(r.id(), r);
            }
        }
        return new ArrayList<>(roleMap.values());
    }

    @Override
    public List<GroupSummary> getEffectiveGroupsForUser(String userId) {
        List<GroupSummary> directGroups = getDirectGroupsForUser(userId);
        Set<UUID> visited = new LinkedHashSet<>();
        List<GroupSummary> all = new ArrayList<>();
        for (GroupSummary g : directGroups) {
            collectAncestors(g.id(), visited, all);
        }
        return all;
    }

    private void collectAncestors(UUID groupId, Set<UUID> visited, List<GroupSummary> result) {
        if (!visited.add(groupId)) return;
        var rows = jdbc.query("SELECT id, name, parent_group_id FROM groups WHERE id = ?",
                (rs, rowNum) -> new Object[]{
                        new GroupSummary(rs.getObject("id", UUID.class), rs.getString("name")),
                        rs.getObject("parent_group_id", UUID.class)
                }, groupId);
        if (rows.isEmpty()) return;
        result.add((GroupSummary) rows.get(0)[0]);
        UUID parentId = (UUID) rows.get(0)[1];
        if (parentId != null) {
            collectAncestors(parentId, visited, result);
        }
    }

    @Override
    public List<RoleSummary> getEffectiveRolesForGroup(UUID groupId) {
        List<RoleSummary> direct = jdbc.query("""
                SELECT r.id, r.name, r.system FROM group_roles gr
                JOIN roles r ON r.id = gr.role_id WHERE gr.group_id = ?
                """, (rs, rowNum) -> new RoleSummary(rs.getObject("id", UUID.class),
                rs.getString("name"), rs.getBoolean("system"), "direct"), groupId);

        Map<UUID, RoleSummary> roleMap = new LinkedHashMap<>();
        for (RoleSummary r : direct) roleMap.put(r.id(), r);

        List<GroupSummary> ancestors = groupRepository.findAncestorChain(groupId);
        for (GroupSummary ancestor : ancestors) {
            if (ancestor.id().equals(groupId)) continue;
            List<RoleSummary> parentRoles = jdbc.query("""
                    SELECT r.id, r.name, r.system FROM group_roles gr
                    JOIN roles r ON r.id = gr.role_id WHERE gr.group_id = ?
                    """, (rs, rowNum) -> new RoleSummary(rs.getObject("id", UUID.class),
                    rs.getString("name"), rs.getBoolean("system"),
                    ancestor.name()), ancestor.id());
            for (RoleSummary r : parentRoles) roleMap.putIfAbsent(r.id(), r);
        }
        return new ArrayList<>(roleMap.values());
    }

    @Override
    public List<UserSummary> getEffectivePrincipalsForRole(UUID roleId) {
        Set<String> seen = new LinkedHashSet<>();
        List<UserSummary> result = new ArrayList<>();

        List<UserSummary> direct = jdbc.query("""
                SELECT u.user_id, u.display_name, u.provider FROM user_roles ur
                JOIN users u ON u.user_id = ur.user_id WHERE ur.role_id = ?
                """, (rs, rowNum) -> new UserSummary(rs.getString("user_id"),
                rs.getString("display_name"), rs.getString("provider")), roleId);
        for (UserSummary u : direct) {
            if (seen.add(u.userId())) result.add(u);
        }

        List<UUID> groupsWithRole = jdbc.query(
                "SELECT group_id FROM group_roles WHERE role_id = ?",
                (rs, rowNum) -> rs.getObject("group_id", UUID.class), roleId);

        Set<UUID> allGroups = new LinkedHashSet<>(groupsWithRole);
        for (UUID gid : groupsWithRole) {
            collectDescendants(gid, allGroups);
        }
        for (UUID gid : allGroups) {
            List<UserSummary> members = jdbc.query("""
                    SELECT u.user_id, u.display_name, u.provider FROM user_groups ug
                    JOIN users u ON u.user_id = ug.user_id WHERE ug.group_id = ?
                    """, (rs, rowNum) -> new UserSummary(rs.getString("user_id"),
                    rs.getString("display_name"), rs.getString("provider")), gid);
            for (UserSummary u : members) {
                if (seen.add(u.userId())) result.add(u);
            }
        }
        return result;
    }

    private void collectDescendants(UUID groupId, Set<UUID> result) {
        List<UUID> children = jdbc.query(
                "SELECT id FROM groups WHERE parent_group_id = ?",
                (rs, rowNum) -> rs.getObject("id", UUID.class), groupId);
        for (UUID child : children) {
            if (result.add(child)) {
                collectDescendants(child, result);
            }
        }
    }

    @Override
    public List<String> getSystemRoleNames(String userId) {
        return getEffectiveRolesForUser(userId).stream()
                .filter(RoleSummary::system)
                .map(RoleSummary::name)
                .toList();
    }

    @Override
    public RbacStats getStats() {
        int userCount = jdbc.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
        int activeUserCount = jdbc.queryForObject(
                "SELECT COUNT(DISTINCT user_id) FROM user_roles", Integer.class);
        int groupCount = jdbc.queryForObject("SELECT COUNT(*) FROM groups", Integer.class);
        int roleCount = jdbc.queryForObject("SELECT COUNT(*) FROM roles", Integer.class);
        int maxDepth = computeMaxGroupDepth();
        return new RbacStats(userCount, activeUserCount, groupCount, maxDepth, roleCount);
    }

    private int computeMaxGroupDepth() {
        List<UUID> roots = jdbc.query(
                "SELECT id FROM groups WHERE parent_group_id IS NULL",
                (rs, rowNum) -> rs.getObject("id", UUID.class));
        int max = 0;
        for (UUID root : roots) {
            max = Math.max(max, measureDepth(root, 1));
        }
        return max;
    }

    private int measureDepth(UUID groupId, int currentDepth) {
        List<UUID> children = jdbc.query(
                "SELECT id FROM groups WHERE parent_group_id = ?",
                (rs, rowNum) -> rs.getObject("id", UUID.class), groupId);
        if (children.isEmpty()) return currentDepth;
        int max = currentDepth;
        for (UUID child : children) {
            max = Math.max(max, measureDepth(child, currentDepth + 1));
        }
        return max;
    }

    @Override
    public List<RoleSummary> getDirectRolesForUser(String userId) {
        return jdbc.query("""
                SELECT r.id, r.name, r.system, ur.origin FROM user_roles ur
                JOIN roles r ON r.id = ur.role_id
                WHERE ur.user_id = ?
                """, (rs, rowNum) -> new RoleSummary(rs.getObject("id", UUID.class),
                rs.getString("name"), rs.getBoolean("system"), rs.getString("origin")), userId);
    }

    private List<GroupSummary> getDirectGroupsForUser(String userId) {
        return jdbc.query("""
                SELECT g.id, g.name FROM user_groups ug
                JOIN groups g ON g.id = ug.group_id WHERE ug.user_id = ?
                """, (rs, rowNum) -> new GroupSummary(rs.getObject("id", UUID.class),
                rs.getString("name")), userId);
    }

    @Override
    public void clearManagedAssignments(String userId) {
        jdbc.update("DELETE FROM user_roles WHERE user_id = ? AND origin = 'managed'", userId);
        jdbc.update("DELETE FROM user_groups WHERE user_id = ? AND origin = 'managed'", userId);
    }

    @Override
    public void assignManagedRole(String userId, UUID roleId, UUID mappingId) {
        jdbc.update("""
                INSERT INTO user_roles (user_id, role_id, origin, mapping_id)
                VALUES (?, ?, 'managed', ?)
                ON CONFLICT (user_id, role_id, origin) DO UPDATE SET mapping_id = EXCLUDED.mapping_id
                """, userId, roleId, mappingId);
    }

    @Override
    public void addUserToManagedGroup(String userId, UUID groupId, UUID mappingId) {
        jdbc.update("""
                INSERT INTO user_groups (user_id, group_id, origin, mapping_id)
                VALUES (?, ?, 'managed', ?)
                ON CONFLICT (user_id, group_id, origin) DO UPDATE SET mapping_id = EXCLUDED.mapping_id
                """, userId, groupId, mappingId);
    }
}
