package de.oaa.xxx.games.chastity.lockee;

import java.security.Principal;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.games.chastity.common.BaseLockRepository;
import de.oaa.xxx.games.chastity.common.CodeCreator;
import de.oaa.xxx.games.chastity.timelock.TimeLockEntity;
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;

@RestController
@RequestMapping("/lockee")
public class LockeeInvitationController {

    private final LockeeInvitationRepository lockeeInvitationRepository;
    private final CardlockRepository cardlockRepository;
    private final BaseLockRepository baseLockRepository;
    private final TimeLockRepository timeLockRepository;
    private final UserRepository userRepository;
    private final SystemMessageService systemMessageService;
    private final UserService userService;

    @Value("${app.base-url:http://localhost:8080}")
    private String baseUrl;

    private static final SecureRandom RNG = new SecureRandom();

    public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
                                      CardlockRepository cardlockRepository,
                                      BaseLockRepository baseLockRepository,
                                      TimeLockRepository timeLockRepository,
                                      UserRepository userRepository,
                                      SystemMessageService systemMessageService,
                                      UserService userService) {
        this.lockeeInvitationRepository = lockeeInvitationRepository;
        this.cardlockRepository         = cardlockRepository;
        this.baseLockRepository         = baseLockRepository;
        this.timeLockRepository         = timeLockRepository;
        this.userRepository             = userRepository;
        this.systemMessageService       = systemMessageService;
        this.userService                = userService;
    }

    private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
        systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
    }

    private String generateUnlockCode(int lines) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < lines; i++) sb.append(RNG.nextInt(10));
        return sb.toString();
    }

    private int randomBetween(Integer min, Integer max) {
        if (max == null) max = 60;
        if (min == null || min >= max) return max;
        return min + new Random().nextInt(max - min);
    }

    @GetMapping("/invitations/mine/count")
    public ResponseEntity<Integer> countMyInvitations(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();
        int count = 0;
        for (var inv : lockeeInvitationRepository.findByLockeeUserId(myId)) {
            var lockOpt = baseLockRepository.findById(inv.getLockId());
            if (lockOpt.isEmpty()) continue;
            if (lockOpt.get().getStartTime() != null) continue;
            count++;
        }
        return ResponseEntity.ok(count);
    }

    @GetMapping("/invitations/mine")
    public ResponseEntity<List<Map<String, Object>>> getMyInvitations(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var invitations = lockeeInvitationRepository.findByLockeeUserId(myId);
        List<Map<String, Object>> result = new ArrayList<>();
        for (var inv : invitations) {
            var lockOpt = baseLockRepository.findById(inv.getLockId());
            if (lockOpt.isEmpty()) continue;
            var lock = lockOpt.get();
            if (lock.getStartTime() != null) continue; // already accepted
            var khOpt = userRepository.findById(inv.getKeyholderUserId());
            if (khOpt.isEmpty()) continue;
            var kh = khOpt.get();
            Map<String, Object> item = new HashMap<>();
            item.put("lockId",              inv.getLockId().toString());
            item.put("lockName",            lock.getName() != null ? lock.getName() : "Unbenanntes Lock");
            item.put("keyholderName",       kh.getName());
            item.put("keyholderProfilePic", kh.getProfilePicture());
            item.put("token",               inv.getToken());
            item.put("createdAt",           inv.getCreatedAt().toString());
            item.put("detailsVisible",      inv.isDetailsVisible());
            result.add(item);
        }
        return ResponseEntity.ok(result);
    }

    @GetMapping("/invitations/sent")
    public ResponseEntity<List<Map<String, Object>>> getSentInvitations(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var invitations = lockeeInvitationRepository.findByKeyholderUserId(myId);
        List<Map<String, Object>> result = new ArrayList<>();
        for (var inv : invitations) {
            var lockOpt = baseLockRepository.findById(inv.getLockId());
            if (lockOpt.isEmpty()) continue;
            var lock = lockOpt.get();
            if (lock.getStartTime() != null) continue; // already accepted
            var lockeeOpt = userRepository.findById(inv.getLockeeUserId());
            if (lockeeOpt.isEmpty()) continue;
            var lockee = lockeeOpt.get();
            Map<String, Object> item = new HashMap<>();
            item.put("lockId",             inv.getLockId().toString());
            item.put("lockName",           lock.getName() != null ? lock.getName() : "Unbenanntes Lock");
            item.put("lockeeName",         lockee.getName());
            item.put("lockeeProfilePic",   lockee.getProfilePicture());
            item.put("token",              inv.getToken());
            item.put("createdAt",          inv.getCreatedAt().toString());
            item.put("detailsVisible",     inv.isDetailsVisible());
            result.add(item);
        }
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/invitations/sent/{token}")
    @Transactional
    public ResponseEntity<Void> cancelSentInvitation(@PathVariable String token, Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var invOpt = lockeeInvitationRepository.findByToken(token);
        if (invOpt.isEmpty()) return ResponseEntity.notFound().build();
        var inv = invOpt.get();
        if (!inv.getKeyholderUserId().equals(myId)) return ResponseEntity.status(403).build();

        var lockOpt = baseLockRepository.findById(inv.getLockId());
        lockeeInvitationRepository.delete(inv);

        if (lockOpt.isPresent()) {
            var lock = lockOpt.get();
            baseLockRepository.delete(lock);
            String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
            sendMessage(myId, inv.getLockeeUserId(),
                    me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
                    null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
        }

        return ResponseEntity.noContent().build();
    }

    @GetMapping("/invitation/{token}")
    public ResponseEntity<Map<String, Object>> getInvitation(@PathVariable String token, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var invOpt = lockeeInvitationRepository.findByToken(token);
        if (invOpt.isEmpty()) return ResponseEntity.notFound().build();
        var inv = invOpt.get();
        if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();

        var lockOpt = baseLockRepository.findById(inv.getLockId());
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var lock = lockOpt.get();

        if (lock.getStartTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_accepted"));

        var khOpt = userRepository.findById(inv.getKeyholderUserId());
        String khName = khOpt.map(u -> u.getName()).orElse("Unbekannt");

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("lockId",         inv.getLockId().toString());
        result.put("lockName",       lock.getName() != null ? lock.getName() : "Unbenanntes Lock");
        result.put("keyholderName",  khName);
        result.put("token",          inv.getToken());
        result.put("createdAt",      inv.getCreatedAt().toString());
        result.put("detailsVisible", inv.isDetailsVisible());

        if (inv.isDetailsVisible() && lock instanceof CardLockEntity cardLock && cardLock.getInitialCards() != null) {
            Map<String, Long> cardCounts = cardLock.getInitialCards().stream()
                    .collect(java.util.stream.Collectors.groupingBy(
                            c -> c.name(), java.util.stream.Collectors.counting()));
            result.put("cardCounts",                   cardCounts);
            result.put("pickEveryMinute",               cardLock.getPickEveryMinute());
            result.put("accumulatePicks",               cardLock.isAccumulatePicks());
            result.put("showRemainingCards",            cardLock.isShowRemainingCards());
            result.put("hygineOpeningEveryMinites",     cardLock.getHygineOpeningEveryMinites());
            result.put("hygineOpeningDurationMinutes",  cardLock.getHygineOpeningDurationMinutes());
            result.put("requiresVerification",          cardLock.isRequiresVerification());
            result.put("taskCount",                     cardLock.getTasks() != null ? cardLock.getTasks().size() : 0);
        }

        return ResponseEntity.ok(result);
    }

    record AcceptRequest(Integer unlockCodeLines) {}

    @PostMapping("/invitation/{token}/accept")
    @Transactional
    public ResponseEntity<Map<String, Object>> acceptInvitation(@PathVariable String token,
                                                                 @RequestBody AcceptRequest req,
                                                                 Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var invOpt = lockeeInvitationRepository.findByToken(token);
        if (invOpt.isEmpty()) return ResponseEntity.notFound().build();
        var inv = invOpt.get();
        if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();

        var lockOpt = baseLockRepository.findById(inv.getLockId());
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var lock = lockOpt.get();
        if (lock.getStartTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_accepted"));

        if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)
                || timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
            return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));

        int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5;
        LocalDateTime now = LocalDateTime.now();

        String unlockCode;
        String lockName;

        if (lock instanceof CardLockEntity cardLock) {
            unlockCode = generateUnlockCode(codeLines);
            cardLock.setStartTime(now);
            cardLock.setUnlockCode(unlockCode);
            cardLock.setUnlockCodeLength(codeLines);
            cardLock.setAvailableCards(new ArrayList<>(cardLock.getInitialCards()));
            cardLock.setOpenPicks(0);
            cardLock.setNextCardIn(now.plusMinutes(cardLock.getPickEveryMinute()));
            if (cardLock.getHygineOpeningEveryMinites() != null) {
                cardLock.setLastHygineOpening(now);
            }
            cardlockRepository.save(cardLock);
            lockName = cardLock.getName() != null && !cardLock.getName().isBlank() ? cardLock.getName() : "Unbenanntes Lock";
        } else if (lock instanceof TimeLockEntity timeLock) {
            unlockCode = CodeCreator.createNumeric(codeLines);
            int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes());
            timeLock.setStartTime(now);
            timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes));
            timeLock.setUnlockCode(unlockCode);
            timeLock.setUnlockCodeLength(codeLines);
            if (timeLock.getHygineOpeningEveryMinites() != null) {
                timeLock.setLastHygineOpening(now);
            }
            timeLockRepository.save(timeLock);
            lockName = timeLock.getName() != null && !timeLock.getName().isBlank() ? timeLock.getName() : "Unbenanntes Lock";
        } else {
            return ResponseEntity.status(500).build();
        }

        lockeeInvitationRepository.delete(inv);

        sendMessage(myId, inv.getKeyholderUserId(),
                me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
                "/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);

        return ResponseEntity.ok(Map.of(
                "lockId",     lock.getLockId().toString(),
                "unlockCode", unlockCode
        ));
    }

    @DeleteMapping("/invitation/{token}")
    @Transactional
    public ResponseEntity<Void> declineInvitation(@PathVariable String token, Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var invOpt = lockeeInvitationRepository.findByToken(token);
        if (invOpt.isEmpty()) return ResponseEntity.notFound().build();
        var inv = invOpt.get();
        if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();

        var lockOpt = baseLockRepository.findById(inv.getLockId());
        lockeeInvitationRepository.delete(inv);

        if (lockOpt.isPresent()) {
            var lock = lockOpt.get();
            baseLockRepository.delete(lock);
            String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
            sendMessage(myId, inv.getKeyholderUserId(),
                    me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
                    null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
        }

        return ResponseEntity.noContent().build();
    }
}
