package com.cameleer.server.app.controller;

import com.cameleer.server.app.dto.SetPasswordRequest;
import com.cameleer.server.app.license.LicenseEnforcer;
import com.cameleer.server.core.admin.AuditCategory;
import com.cameleer.server.core.admin.AuditResult;
import com.cameleer.server.core.admin.AuditService;
import com.cameleer.server.core.rbac.RbacService;
import com.cameleer.server.core.rbac.SystemRole;
import com.cameleer.server.core.rbac.UserDetail;
import com.cameleer.server.core.security.PasswordPolicyValidator;
import com.cameleer.server.core.security.UserInfo;
import com.cameleer.server.core.security.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import com.cameleer.server.app.security.SecurityProperties;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;


/**
 * Admin endpoints for user management.
 * Protected by {@code ROLE_ADMIN}.
 */
@RestController
@RequestMapping("/api/v1/admin/users")
@Tag(name = "User Admin", description = "User management (ADMIN only)")
@PreAuthorize("hasRole('ADMIN')")
public class UserAdminController {

    private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    private final RbacService rbacService;
    private final UserRepository userRepository;
    private final AuditService auditService;
    private final LicenseEnforcer licenseEnforcer;
    private final boolean oidcEnabled;

    public UserAdminController(RbacService rbacService, UserRepository userRepository,
                               AuditService auditService, SecurityProperties securityProperties,
                               LicenseEnforcer licenseEnforcer) {
        this.rbacService = rbacService;
        this.userRepository = userRepository;
        this.auditService = auditService;
        this.licenseEnforcer = licenseEnforcer;
        String issuer = securityProperties.getOidc().getIssuerUri();
        this.oidcEnabled = issuer != null && !issuer.isBlank();
    }

    @GetMapping
    @Operation(summary = "List all users with RBAC detail")
    @ApiResponse(responseCode = "200", description = "User list returned")
    public ResponseEntity<List<UserDetail>> listUsers(HttpServletRequest httpRequest) {
        auditService.log("view_users", AuditCategory.USER_MGMT, null, null, AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok(rbacService.listUsers());
    }

    @GetMapping("/{userId}")
    @Operation(summary = "Get user by ID with RBAC detail")
    @ApiResponse(responseCode = "200", description = "User found")
    @ApiResponse(responseCode = "404", description = "User not found")
    public ResponseEntity<UserDetail> getUser(@PathVariable String userId) {
        UserDetail detail = rbacService.getUser(userId);
        if (detail == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(detail);
    }

    @PostMapping
    @Operation(summary = "Create a local user")
    @ApiResponse(responseCode = "200", description = "User created")
    @ApiResponse(responseCode = "400", description = "Disabled in OIDC mode")
    public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request,
                                                  HttpServletRequest httpRequest) {
        // License cap fires first so over-cap creates short-circuit before any other validation.
        // Audit emission for the rejection is handled inside LicenseEnforcer (3-arg ctor wires AuditService).
        licenseEnforcer.assertWithinCap("max_users", userRepository.count(), 1);
        if (oidcEnabled) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", "Local user creation is disabled when OIDC is enabled. Users are provisioned automatically via SSO."));
        }
        // DB key is the bare username (matches alert_rules.created_by FK shape used by
        // the env-scoped read-path controllers, which strip "user:" from JWT subjects).
        String userId = request.username();
        UserInfo user = new UserInfo(userId, "local",
                request.email() != null ? request.email() : "",
                request.displayName() != null ? request.displayName() : request.username(),
                Instant.now());
        userRepository.upsert(user);
        if (request.password() != null && !request.password().isBlank()) {
            List<String> violations = PasswordPolicyValidator.validate(request.password(), request.username());
            if (!violations.isEmpty()) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                        "Password policy violation: " + String.join("; ", violations));
            }
            userRepository.setPassword(userId, passwordEncoder.encode(request.password()));
        }
        rbacService.assignRoleToUser(userId, SystemRole.VIEWER_ID);
        auditService.log("create_user", AuditCategory.USER_MGMT, userId,
                Map.of("username", request.username()), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok(rbacService.getUser(userId));
    }

    @PutMapping("/{userId}")
    @Operation(summary = "Update user display name or email")
    @ApiResponse(responseCode = "200", description = "User updated")
    @ApiResponse(responseCode = "404", description = "User not found")
    public ResponseEntity<Void> updateUser(@PathVariable String userId,
                                            @RequestBody UpdateUserRequest request,
                                            HttpServletRequest httpRequest) {
        var existing = userRepository.findById(userId);
        if (existing.isEmpty()) return ResponseEntity.notFound().build();
        var user = existing.get();
        var updated = new UserInfo(user.userId(), user.provider(),
                request.email() != null ? request.email() : user.email(),
                request.displayName() != null ? request.displayName() : user.displayName(),
                user.createdAt());
        userRepository.upsert(updated);
        auditService.log("update_user", AuditCategory.USER_MGMT, userId,
                null, AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok().build();
    }

    @PostMapping("/{userId}/roles/{roleId}")
    @Operation(summary = "Assign a role to a user")
    @ApiResponse(responseCode = "200", description = "Role assigned")
    @ApiResponse(responseCode = "404", description = "User or role not found")
    public ResponseEntity<Void> assignRoleToUser(@PathVariable String userId,
                                                  @PathVariable UUID roleId,
                                                  HttpServletRequest httpRequest) {
        rbacService.assignRoleToUser(userId, roleId);
        auditService.log("assign_role_to_user", AuditCategory.USER_MGMT, userId,
                Map.of("roleId", roleId), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/{userId}/roles/{roleId}")
    @Operation(summary = "Remove a role from a user")
    @ApiResponse(responseCode = "204", description = "Role removed")
    public ResponseEntity<Void> removeRoleFromUser(@PathVariable String userId,
                                                    @PathVariable UUID roleId,
                                                    HttpServletRequest httpRequest) {
        rbacService.removeRoleFromUser(userId, roleId);
        auditService.log("remove_role_from_user", AuditCategory.USER_MGMT, userId,
                Map.of("roleId", roleId), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.noContent().build();
    }

    @PostMapping("/{userId}/groups/{groupId}")
    @Operation(summary = "Add a user to a group")
    @ApiResponse(responseCode = "200", description = "User added to group")
    public ResponseEntity<Void> addUserToGroup(@PathVariable String userId,
                                                @PathVariable UUID groupId,
                                                HttpServletRequest httpRequest) {
        rbacService.addUserToGroup(userId, groupId);
        auditService.log("add_user_to_group", AuditCategory.USER_MGMT, userId,
                Map.of("groupId", groupId), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/{userId}/groups/{groupId}")
    @Operation(summary = "Remove a user from a group")
    @ApiResponse(responseCode = "204", description = "User removed from group")
    public ResponseEntity<Void> removeUserFromGroup(@PathVariable String userId,
                                                     @PathVariable UUID groupId,
                                                     HttpServletRequest httpRequest) {
        rbacService.removeUserFromGroup(userId, groupId);
        auditService.log("remove_user_from_group", AuditCategory.USER_MGMT, userId,
                Map.of("groupId", groupId), AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping("/{userId}")
    @Operation(summary = "Delete user")
    @ApiResponse(responseCode = "204", description = "User deleted")
    @ApiResponse(responseCode = "409", description = "Cannot delete the last admin user")
    public ResponseEntity<Void> deleteUser(@PathVariable String userId,
                                            HttpServletRequest httpRequest) {
        boolean isAdmin = rbacService.getEffectiveRolesForUser(userId).stream()
                .anyMatch(r -> r.id().equals(SystemRole.ADMIN_ID));
        if (isAdmin && rbacService.getEffectivePrincipalsForRole(SystemRole.ADMIN_ID).size() <= 1) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Cannot delete the last admin user");
        }
        userRepository.delete(userId);
        auditService.log("delete_user", AuditCategory.USER_MGMT, userId,
                null, AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.noContent().build();
    }

    @PostMapping("/{userId}/password")
    @Operation(summary = "Reset user password")
    @ApiResponse(responseCode = "204", description = "Password reset")
    @ApiResponse(responseCode = "400", description = "Disabled in OIDC mode or policy violation")
    public ResponseEntity<Void> resetPassword(
            @PathVariable String userId,
            @Valid @RequestBody SetPasswordRequest request,
            HttpServletRequest httpRequest) {
        // Block local UI users from resetting passwords when OIDC is enabled,
        // but allow M2M callers (SaaS platform) identified by "oidc:" principal prefix
        if (oidcEnabled) {
            String caller = httpRequest.getUserPrincipal() != null ? httpRequest.getUserPrincipal().getName() : "";
            if (!caller.startsWith("oidc:")) {
                return ResponseEntity.badRequest().build();
            }
        }
        List<String> violations = PasswordPolicyValidator.validate(request.password(), userId);
        if (!violations.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "Password policy violation: " + String.join("; ", violations));
        }
        userRepository.setPassword(userId, passwordEncoder.encode(request.password()));
        // Revoke all existing tokens so the user must re-authenticate with the new password
        userRepository.revokeTokensBefore(userId, Instant.now());
        auditService.log("reset_password", AuditCategory.USER_MGMT, userId, null, AuditResult.SUCCESS, httpRequest);
        return ResponseEntity.noContent().build();
    }

    public record CreateUserRequest(String username, String displayName, String email, String password) {}
    public record UpdateUserRequest(String displayName, String email) {}
}
