package com.cameleer.server.app.security;

import com.cameleer.server.app.dto.AuthTokenResponse;
import com.cameleer.server.app.dto.ErrorResponse;
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.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
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.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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 java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Authentication endpoints for the UI (local credentials).
 * <p>
 * Validates credentials against environment-configured username/password,
 * then issues JWTs with {@code user:} prefixed subjects and ADMIN roles.
 * Upserts the user into the user store on login.
 */
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "Authentication", description = "Login and token refresh endpoints")
public class UiAuthController {

    private static final Logger log = LoggerFactory.getLogger(UiAuthController.class);
    private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    private final JwtService jwtService;
    private final SecurityProperties properties;
    private final UserRepository userRepository;
    private final AuditService auditService;
    private final RbacService rbacService;

    public UiAuthController(JwtService jwtService, SecurityProperties properties,
                             UserRepository userRepository, AuditService auditService,
                             RbacService rbacService) {
        this.jwtService = jwtService;
        this.properties = properties;
        this.userRepository = userRepository;
        this.auditService = auditService;
        this.rbacService = rbacService;
    }

    @PostMapping("/login")
    @Operation(summary = "Login with local credentials")
    @ApiResponse(responseCode = "200", description = "Login successful")
    @ApiResponse(responseCode = "401", description = "Invalid credentials",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    @ApiResponse(responseCode = "429", description = "Account locked due to too many failed attempts")
    public ResponseEntity<AuthTokenResponse> login(@RequestBody LoginRequest request,
                                                       HttpServletRequest httpRequest) {
        String configuredUser = properties.getUiUser();
        String configuredPassword = properties.getUiPassword();
        // The JWT subject carries a "user:" namespace prefix so the auth filter
        // can distinguish user vs agent tokens. The DB row keys (users.user_id,
        // user_roles.user_id, alert_rules.created_by FK, …) are the bare username:
        // every env-scoped controller strips the prefix on the read path via
        // stripSubjectPrefix(...), so the write path here must match.
        String userId  = request.username();
        String subject = "user:" + userId;

        if (userRepository.isLocked(userId)) {
            auditService.log(request.username(), "login_locked", AuditCategory.AUTH, null,
                    Map.of("reason", "Account locked"), AuditResult.FAILURE, httpRequest);
            throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS,
                    "Account locked due to too many failed attempts. Try again later.");
        }

        boolean envMatch = configuredUser != null && !configuredUser.isBlank()
                && configuredPassword != null && !configuredPassword.isBlank()
                && configuredUser.equals(request.username())
                && configuredPassword.equals(request.password());

        if (!envMatch) {
            Optional<String> hash = userRepository.getPasswordHash(userId);
            if (hash.isEmpty() || !passwordEncoder.matches(request.password(), hash.get())) {
                userRepository.recordFailedLogin(userId);
                log.debug("UI login failed for user: {}", request.username());
                auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
                        Map.of("reason", "Invalid credentials"), AuditResult.FAILURE, httpRequest);
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
            }
        }

        userRepository.clearFailedLogins(userId);

        if (envMatch) {
            // Env-var admin: upsert unprefixed and ensure ADMIN role + Admins group
            try {
                userRepository.upsert(new UserInfo(
                        userId, "local", "", request.username(), Instant.now()));
                rbacService.assignRoleToUser(userId, SystemRole.ADMIN_ID);
                rbacService.addUserToGroup(userId, SystemRole.ADMINS_GROUP_ID);
            } catch (Exception e) {
                log.warn("Failed to upsert local admin to store (login continues): {}", e.getMessage());
            }
        }
        // Per-user logins: user already exists in DB (created by admin)

        List<String> roles = rbacService.getSystemRoleNames(userId);
        if (roles.isEmpty()) {
            roles = List.of("VIEWER");
        }

        String accessToken = jwtService.createAccessToken(subject, "user", roles);
        String refreshToken = jwtService.createRefreshToken(subject, "user", roles);

        auditService.log(request.username(), "login", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
        log.info("UI user logged in: {}", request.username());
        return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username(), null));
    }

    @PostMapping("/refresh")
    @Operation(summary = "Refresh access token")
    @ApiResponse(responseCode = "200", description = "Token refreshed")
    @ApiResponse(responseCode = "401", description = "Invalid refresh token",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request,
                                                        HttpServletRequest httpRequest) {
        try {
            JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
            if (!result.subject().startsWith("user:")) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not a UI token");
            }

            // Preserve roles from the refresh token
            List<String> roles = result.roles();
            String accessToken = jwtService.createAccessToken(result.subject(), "user", roles);
            String refreshToken = jwtService.createRefreshToken(result.subject(), "user", roles);

            String userId = stripSubjectPrefix(result.subject());
            String displayName = userRepository.findById(userId)
                    .map(UserInfo::displayName)
                    .orElse(userId);
            auditService.log(result.subject(), "token_refresh", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
            return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null));
        } catch (ResponseStatusException e) {
            throw e;
        } catch (Exception e) {
            log.debug("UI token refresh failed: {}", e.getMessage());
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid refresh token");
        }
    }

    @GetMapping("/me")
    @Operation(summary = "Get current user details")
    @ApiResponse(responseCode = "200", description = "Current user details")
    @ApiResponse(responseCode = "401", description = "Not authenticated")
    public ResponseEntity<UserDetail> me(Authentication authentication) {
        if (authentication == null || authentication.getName() == null) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not authenticated");
        }
        UserDetail detail = rbacService.getUser(stripSubjectPrefix(authentication.getName()));
        if (detail == null) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found");
        }
        return ResponseEntity.ok(detail);
    }

    /**
     * Map a JWT subject ({@code "user:<name>"} or {@code "user:oidc:<sub>"}) to the DB key:
     * just the bare username. FKs on {@code alert_rules.created_by},
     * {@code outbound_connections.created_by}, etc. reference the unprefixed row.
     */
    private static String stripSubjectPrefix(String subject) {
        return subject != null && subject.startsWith("user:") ? subject.substring(5) : subject;
    }

    public record LoginRequest(String username, String password) {}
    public record RefreshRequest(String refreshToken) {}
}
