package com.alttd.altitudeweb.controllers.login;

import com.alttd.altitudeweb.api.LoginApi;
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.litebans.RecentNamesMapper;
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
import com.alttd.altitudeweb.model.PermissionClaimDto;
import com.alttd.altitudeweb.model.UsernameDto;
import com.alttd.altitudeweb.services.limits.RateLimit;
import com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

@Slf4j
@RequiredArgsConstructor
@EnableScheduling
@RestController
public class LoginController implements LoginApi {

    private final JwtEncoder jwtEncoder;
    private final AuthenticatedUuid authenticatedUuid;

    @Value("${login.secret:#{null}}")
    private String loginSecret;

    @Value("${my-server.address:#{null}}")
    private String serverAddress;

    private record CacheEntry(UUID uuid, Instant expiry) {
    }

    private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();

    @Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
    protected void clearExpiredCacheEntries() {
        Instant now = Instant.now();
        int initialCacheSize = cache.size();
        cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
        log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
    }

    @RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
    @Override
    public ResponseEntity<String> requestLogin(String authorization, String uuid) {
        UUID uuidFromString;
        try {
            uuidFromString = UUID.fromString(uuid);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }

        log.info("{} is requesting a login code", uuid);

        if (authorization == null || !authorization.startsWith("SECRET ")) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }

        String secret = authorization.substring("SECRET ".length());
        if (!isValidSecret(secret)) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid secret");
        }

        Optional<String> key = cache.entrySet().stream()
                                    .filter(entry -> entry.getValue().uuid.equals(uuidFromString))
                                    .map(Map.Entry::getKey)
                                    .findFirst();

        if (key.isPresent()) {
            log.info("{} got cached key: {}", uuid,  key.get());
            return ResponseEntity.ok(key.get());
        }

        String loginCode = generateLoginCode(uuidFromString);
        log.info("{} received login code: {}", uuid, loginCode);
        return ResponseEntity.ok(loginCode);
    }

    @Override
    public ResponseEntity<UsernameDto> getUsername() {
        log.debug("Loading username for logged in user");

        try {
            // Get authenticated UUID using the utility method
            UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
            log.debug("Loaded username for logged in user {}", uuid);

            // Create response with username
            UsernameDto usernameDto = new UsernameDto();
            usernameDto.setUsername(getUsername(uuid));
            log.debug("Loaded username for logged in user {}", usernameDto.getUsername());

            return ResponseEntity.ok(usernameDto);

        } catch (ResponseStatusException e) {
            // The utility method already throws proper exceptions, we just need to convert them to ResponseEntity
            return ResponseEntity.status(e.getStatusCode()).build();
        }
    }

    private String getUsername(UUID uuid) {
        CompletableFuture<String> username = new CompletableFuture<>();

        Connection.getConnection(Databases.LITE_BANS)
                  .runQuery(sqlSession -> {
                      log.debug("Loading all history through logged in uuid");
                      try {
                          String temp = sqlSession
                                  .getMapper(RecentNamesMapper.class)
                                  .getUsername(uuid.toString());
                          username.complete(temp);
                      } catch (Exception e) {
                          log.error("Failed to find username for uuid {}", uuid, e);
                          username.completeExceptionally(e);
                      }
                  });
        return username.join();
    }

    @Value("${UNSECURED:#{false}}")
    private boolean unsecured;

    @RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
    @Override
    public ResponseEntity<String> login(String code) {
        if (unsecured) {
            log.warn("Unsecured login is enabled, skipping login validation!");
        } else {
            log.info("Received login request with code {}", code);
        }
        if (code == null) {
            log.warn("Received null login code");
            return ResponseEntity.badRequest().build();
        }

        CacheEntry cacheEntry = cache.get(code);
        if (!unsecured && (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now()))) {
            log.warn("Received invalid login code {}", code);
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }

        if (unsecured && cacheEntry == null) {
            cacheEntry = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
        }

        String token = generateToken(cacheEntry.uuid);
        log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);

        cache.remove(code);

        log.debug("Generated token for user {}", cacheEntry.uuid);
        return ResponseEntity.ok(token);
    }

    private String generateLoginCode(UUID uuid) {
        String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        StringBuilder loginCode = new StringBuilder();
        for (int i = 0; i < 8; i++) {
            int index = (int) (Math.random() * characters.length());
            loginCode.append(characters.charAt(index));
        }
        CacheEntry cacheEntry = new CacheEntry(uuid,
                Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
        cache.put(loginCode.toString(), cacheEntry);
        return loginCode.toString();
    }

    private boolean isValidSecret(String secret) {
        if (loginSecret == null) {
            log.warn("No login secret set, skipping secret validation");
            return false;
        }
        if (loginSecret.length() < 16) {
            log.warn("Login secret is too short, skipping secret validation");
            return false;
        }
        if (!loginSecret.equals(secret)) {
            log.info("Received invalid secret attempt");
            return false;
        }
        return true;
    }

    private String generateToken(UUID uuid) {
        Instant now = Instant.now();
        //TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
        Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
        CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
        List<PermissionClaimDto> claimList = new ArrayList<>();
        Connection.getConnection(Databases.DEFAULT)
                  .runQuery(sqlSession -> {
                      try {
                          log.debug("Loading user by uuid {}", uuid.toString());
                          PrivilegedUserMapper mapper = sqlSession.getMapper(PrivilegedUserMapper.class);
                          Optional<PrivilegedUser> optionalPrivilegedUser = mapper
                                  .getUserByUuid(uuid);

                          if (optionalPrivilegedUser.isEmpty()) {
                              PrivilegedUser privilegedUser = new PrivilegedUser(null, uuid, List.of());
                              mapper.createPrivilegedUser(privilegedUser);
                              privilegedUserCompletableFuture.complete(
                                      Optional.of(privilegedUser));
                          } else {
                              privilegedUserCompletableFuture.complete(optionalPrivilegedUser);
                          }
                      } catch (Exception e) {
                          log.error("Failed to load user by uuid", e);
                          privilegedUserCompletableFuture.completeExceptionally(e);
                      }
                  });
        Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
        claimList.add(PermissionClaimDto.USER);
        privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
            try {
                claimList.add(PermissionClaimDto.fromValue(permission));
                log.debug("Added permission claim {}", permission);
            } catch (IllegalArgumentException e) {
                log.warn("Received invalid permission claim: {}", permission);
            }
        }));
        log.debug("Generated token for user {} with claims {}", uuid.toString(),
                claimList.stream().map(PermissionClaimDto::getValue).toList());
        JwtClaimsSet claims = JwtClaimsSet.builder()
                                          .issuer(serverAddress)
                                          .claim("authorities",
                                                  claimList.stream().map(PermissionClaimDto::getValue).toList())
                                          .issuedAt(now)
                                          .expiresAt(expiryTime)
                                          .subject(uuid.toString())
                                          .build();

        return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}
