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

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Principal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.imageio.ImageIO;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteEntity;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.lockcontroll.LockControllType;
import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;

@RestController
@RequestMapping("/keyholder")
public class TimeLockController {

    private final TimeLockRepository timeLockRepository;
    private final TimeLockTemplateRepository templateRepository;
    private final UserRepository userRepository;
    private final UserService userService;
    private final KeyholderInvitationRepository invitationRepository;
    private final LockeeInvitationRepository lockeeInvitationRepository;
    private final SystemMessageService systemMessageService;
    private final TimeLockServiceFactory timeLockServiceFactory;
    private final CommunityVerificationRepository verificationRepository;
    private final CommunityVerificationVoteRepository verificationVoteRepository;
    private final SubscriptionLimitService subscriptionLimitService;
    private final UnlockCodeHistoryService unlockCodeHistoryService;

    public TimeLockController(TimeLockRepository timeLockRepository,
                              TimeLockTemplateRepository templateRepository,
                              UserRepository userRepository,
                              UserService userService,
                              KeyholderInvitationRepository invitationRepository,
                              LockeeInvitationRepository lockeeInvitationRepository,
                              SystemMessageService systemMessageService,
                              TimeLockServiceFactory timeLockServiceFactory,
                              CommunityVerificationRepository verificationRepository,
                              CommunityVerificationVoteRepository verificationVoteRepository,
                              SubscriptionLimitService subscriptionLimitService,
                              UnlockCodeHistoryService unlockCodeHistoryService) {
        this.timeLockRepository       = timeLockRepository;
        this.templateRepository       = templateRepository;
        this.userRepository           = userRepository;
        this.userService              = userService;
        this.invitationRepository     = invitationRepository;
        this.lockeeInvitationRepository = lockeeInvitationRepository;
        this.systemMessageService     = systemMessageService;
        this.timeLockServiceFactory   = timeLockServiceFactory;
        this.verificationRepository   = verificationRepository;
        this.verificationVoteRepository = verificationVoteRepository;
        this.subscriptionLimitService = subscriptionLimitService;
        this.unlockCodeHistoryService = unlockCodeHistoryService;
    }

    // ── Erstellen ────────────────────────────────────────────────────────────────

    record CreateTimeLockRequest(
            UUID templateId,
            UUID lockeeUserId,
            boolean lockeeDetailsVisible,
            UUID keyholder,
            boolean testLock,
            Integer unlockCodeLength,
            LockControllType controllType
    ) {}

    @PostMapping("/timelock")
    @Transactional
    public ResponseEntity<Map<String, Object>> createTimeLock(
            @RequestBody CreateTimeLockRequest req, Principal principal) {

        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var templateOpt = templateRepository.findById(req.templateId());
        if (templateOpt.isEmpty()) return ResponseEntity.badRequest().build();
        var template = templateOpt.get();

        int codeLen = (req.unlockCodeLength() != null && req.unlockCodeLength() >= 1) ? req.unlockCodeLength() : 5;
        boolean friendLockee = req.lockeeUserId() != null && !req.lockeeUserId().equals(myId);

        if (friendLockee) {
            var lockeeOpt = userRepository.findById(req.lockeeUserId());
            if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build();
            var lockee = lockeeOpt.get();

            if (timeLockServiceFactory.hasActiveLock(req.lockeeUserId()))
                return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));

            TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
            lock.setStartTime(null);
            timeLockRepository.save(lock);

            String token = UUID.randomUUID().toString().replace("-", "");
            LockeeInvitationEntity inv = new LockeeInvitationEntity();
            inv.setLockId(lock.getLockId());
            inv.setLockeeUserId(lockee.getUserId());
            inv.setKeyholderUserId(myId);
            inv.setToken(token);
            inv.setCreatedAt(LocalDateTime.now());
            inv.setDetailsVisible(req.lockeeDetailsVisible());
            lockeeInvitationRepository.save(inv);

            String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
            systemMessageService.send(myId, lockee.getUserId(),
                    me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
                    "/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);

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

        if (timeLockServiceFactory.hasActiveLock(myId))
            return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));

        LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
        if (controllType == LockControllType.TTLOCK && !subscriptionLimitService.hasActivePaidSubscription(myId)) {
            return ResponseEntity.status(403).body(Map.of("error", "subscription_required"));
        }

        TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings(
                req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE,
                myId, req.keyholder(), req.testLock(), codeLen);
        TimeLockEntity lock = new TimeLockEntity();
        timeLockServiceFactory.create(lock).init(template, settings);
        timeLockRepository.save(lock); // Sicherstellen dass auch TRUST-Locks persistiert sind

        boolean keyholderPending = false;
        if (req.keyholder() != null) {
            var khOpt = userRepository.findById(req.keyholder());
            if (khOpt.isPresent()) {
                var kh = khOpt.get();
                KeyholderInvitationEntity inv = new KeyholderInvitationEntity();
                inv.setLockId(lock.getLockId());
                inv.setKeyholderUserId(kh.getUserId());
                inv.setLockeeUserId(myId);
                inv.setToken(UUID.randomUUID().toString().replace("-", ""));
                inv.setCreatedAt(LocalDateTime.now());
                invitationRepository.save(inv);

                String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
                systemMessageService.send(myId, kh.getUserId(),
                        me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
                        "/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);

                keyholderPending = true;
            }
        }

        return ResponseEntity.ok(Map.of(
                "lockId",           lock.getLockId().toString(),
                "unlockCode",       lock.getUnlockCode() != null ? lock.getUnlockCode() : "",
                "keyholderPending", keyholderPending));
    }

    // ── State abrufen ────────────────────────────────────────────────────────────

    @GetMapping("/timelock/{lockId}")
    @Transactional
    public ResponseEntity<Map<String, Object>> getTimeLock(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();

        LocalDateTime now = LocalDateTime.now();

        // Periodic check (daily violations)
        TimeLockService service = timeLockServiceFactory.create(l);
        service.check();

        // Auto-unfreeze when frozenUntil has passed
        if (l.getFrozenUntil() != null && l.getFrozenUntil().isBefore(now) && l.getFrozenFrom() != null) {
            service.unfreeze();
            l.setFrozenFrom(null);
            l.setFrozenUntil(null);
            timeLockRepository.save(l);
        }

        // Emergency auto-unlock after 1h
        if (l.getEmergencyUnlockRequestedAt() != null && !l.isKeyholderRequestedUnlock()
                && l.getEmergencyUnlockRequestedAt().isBefore(now.minusHours(1))) {
            l.setEmergencyAutoUnlocked(true);
            l.setKeyholderRequestedUnlock(true);
            timeLockRepository.save(l);
        }

        boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
        boolean isFrozen = l.getFrozenFrom() != null
                && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));

        // Hygiene state
        boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null;
        boolean hygieneOpeningDue = false;
        long hygieneSecondsRemaining = 0;
        boolean hygieneOpeningActive = l.getTempOpeningTime() != null
                && TempOpeningReason.HYGIENE == l.getTempOpeningReason();
        if (hygieneEnabled && !hygieneOpeningActive) {
            LocalDateTime lastH = l.getLastHygineOpening();
            if (lastH == null) {
                hygieneOpeningDue = true;
            } else {
                LocalDateTime nextH = lastH.plusMinutes(l.getHygineOpeningEveryMinites());
                long secs = ChronoUnit.SECONDS.between(now, nextH);
                if (secs <= 0) hygieneOpeningDue = true;
                else hygieneSecondsRemaining = secs;
            }
        }

        // Spin wheel state
        boolean spinEnabled = l.getSpinsEveryMinutes() != null
                && l.getSpinningWheelEntries() != null && !l.getSpinningWheelEntries().isEmpty();
        boolean spinDue = false;
        String nextSpinIn = null;
        if (spinEnabled) {
            List<LocalDateTime> times = l.getSpinningWheelTimes();
            if (times == null || times.isEmpty()) {
                spinDue = true;
            } else {
                LocalDateTime next = times.get(times.size() - 1).plusMinutes(l.getSpinsEveryMinutes());
                if (next.isBefore(now)) spinDue = true;
                else nextSpinIn = next.toString();
            }
        }

        // Task timing state
        boolean taskTimingEnabled = l.getTaskEveryMinutes() != null;
        String nextTaskIn = null;
        if (taskTimingEnabled && l.getCurrentTask() == null) {
            List<LocalDateTime> times = l.getTaskTimes();
            LocalDateTime next;
            if (times == null || times.isEmpty()) {
                next = l.getStartTime() != null
                        ? l.getStartTime().plusMinutes(l.getTaskEveryMinutes()) : null;
            } else {
                next = times.get(times.size() - 1).plusMinutes(l.getTaskEveryMinutes());
            }
            if (next != null && next.isAfter(now)) nextTaskIn = next.toString();
        }

        // Keyholder info
        boolean keyholderInvitationPending =
                l.getKeyholder() == null && !invitationRepository.findByLockId(lockId).isEmpty();

        // Verification state
        boolean verificationDue = false;
        String verificationPendingId = null;
        String verificationPendingCode = null;
        long verificationUpvotes = 0;
        long verificationDownvotes = 0;
        if (l.isRequiresVerification()) {
            LocalDateTime todayStart = LocalDate.now().atStartOfDay();
            LocalDateTime todayEnd = todayStart.plusDays(1);
            var completed = verificationRepository
                    .findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId, todayStart, todayEnd);
            if (!completed.isEmpty()) {
                var todayV = completed.get(0);
                var votes = verificationVoteRepository.findAllByVerificationId(todayV.getDisplayId());
                verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
                verificationDownvotes = votes.stream().filter(v -> !v.isUpvote()).count();
            } else {
                verificationDue = true;
                var pending = verificationRepository
                        .findByLockIdAndCreatedAtBetweenAndImageIsNull(lockId, todayStart, todayEnd);
                if (!pending.isEmpty()) {
                    verificationPendingId = pending.get(0).getDisplayId().toString();
                    verificationPendingCode = pending.get(0).getCode();
                }
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("lockId",       l.getLockId().toString());
        result.put("name",         l.getName() != null ? l.getName() : "");
        result.put("testLock",     l.isTestLock());
        result.put("startTime",    l.getStartTime() != null ? l.getStartTime().toString() : null);
        result.put("endTimeVisible", l.isEndTimeVisible());
        result.put("timeUp",       timeUp);
        result.put("isFrozen",     isFrozen);
        result.put("frozenUntil",  l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);

        // Only expose unlock time if end time is visible OR time is up
        if (l.isEndTimeVisible() || timeUp) {
            result.put("unlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null);
        } else {
            result.put("unlockTime", null);
        }

        result.put("currentTask",            l.getCurrentTask());
        result.put("currentTaskDescription", l.getCurrentTaskDescription());
        result.put("taskUntil",              l.getTaskUntil() != null ? l.getTaskUntil().toString() : null);

        result.put("spinEnabled",  spinEnabled);
        result.put("spinDue",      spinDue);
        result.put("nextSpinIn",   nextSpinIn);
        result.put("spinningWheelEntries", l.getSpinningWheelEntries() != null ? l.getSpinningWheelEntries() : List.of());

        result.put("taskTimingEnabled", taskTimingEnabled);
        result.put("nextTaskIn",        nextTaskIn);

        result.put("hygieneEnabled",          hygieneEnabled);
        result.put("hygieneOpeningDue",       hygieneOpeningDue);
        result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
        result.put("hygieneOpeningActive",    hygieneOpeningActive);
        result.put("hygieneOpeningStarted",   l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
        result.put("hygieneDurationMinutes",  l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0);

        result.put("verificationRequired",    l.isRequiresVerification());
        result.put("verificationDue",         verificationDue);
        result.put("verificationPendingId",   verificationPendingId);
        result.put("verificationPendingCode", verificationPendingCode);
        result.put("verificationUpvotes",     verificationUpvotes);
        result.put("verificationDownvotes",   verificationDownvotes);

        result.put("hasKeyholder",              l.getKeyholder() != null);
        result.put("keyholderInvitationPending", keyholderInvitationPending);
        if (l.getKeyholder() != null) {
            userRepository.findById(l.getKeyholder()).ifPresent(kh -> {
                result.put("keyholderName",       kh.getName());
                result.put("keyholderUserId",     kh.getUserId().toString());
                result.put("keyholderProfilePic", kh.getProfilePicture());
            });
        }

        result.put("controllType", l.getControllType() != null ? l.getControllType().name() : "UNLOCK_CODE");
        result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
        if (l.isKeyholderRequestedUnlock() || l.isTestLock() || timeUp) {
            result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
        }
        result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
        result.put("emergencyAutoUnlocked",    l.isEmergencyAutoUnlocked());
        result.put("actualUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);

        return ResponseEntity.ok(result);
    }

    // ── Glücksrad drehen ─────────────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/spin")
    @Transactional
    public ResponseEntity<Map<String, Object>> spin(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.getEstimatedUnlockTime() == null) return ResponseEntity.status(409).build(); // not started
        if (l.getSpinningWheelEntries() == null || l.getSpinningWheelEntries().isEmpty())
            return ResponseEntity.status(409).build();

        LocalDateTime now = LocalDateTime.now();
        boolean isFrozen = l.getFrozenFrom() != null
                && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
        if (isFrozen) return ResponseEntity.status(409).body(Map.of("error", "frozen"));
        if (TempOpeningReason.HYGIENE == l.getTempOpeningReason())
            return ResponseEntity.status(409).body(Map.of("error", "hygiene_opening"));

        // Check spin is due
        List<LocalDateTime> spinTimes = l.getSpinningWheelTimes();
        if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEveryMinutes() != null) {
            LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusMinutes(l.getSpinsEveryMinutes());
            if (next.isAfter(now)) return ResponseEntity.status(409).body(Map.of("error", "not_due"));
        }

        TimeLockService service = timeLockServiceFactory.create(l);
        SpinningWheelEntry entry = service.spinWheel();
        if (entry == null) return ResponseEntity.status(409).body(Map.of("error", "no_entry"));

        // Record spin time
        if (l.getSpinningWheelTimes() == null) l.setSpinningWheelTimes(new ArrayList<>());
        l.getSpinningWheelTimes().add(now);
        timeLockRepository.save(l);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("type",      entry.getType().name());
        result.put("intVal",    entry.getIntVal());
        result.put("stringVal", entry.getStringVal());
        // Include updated lock time fields
        result.put("newUnlockTime",  l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null);
        result.put("newFrozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
        result.put("isFrozen",       l.getFrozenFrom() != null);
        result.put("currentTask",    l.getCurrentTask());
        return ResponseEntity.ok(result);
    }

    // ── Aufgabe erledigt ──────────────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/relock")
    @Transactional
    public ResponseEntity<Void> relock(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.getControllType() != LockControllType.TTLOCK) return ResponseEntity.status(409).build();

        var lc = timeLockServiceFactory.create(l).getLockControl();
        if (lc != null) lc.lock();
        return ResponseEntity.noContent().build();
    }

    @PostMapping("/timelock/{lockId}/task/done")
    @Transactional
    public ResponseEntity<Void> taskDone(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.getCurrentTask() == null) return ResponseEntity.status(409).build();

        // Check task timer hasn't expired (still locked)
        if (l.getTaskUntil() != null && l.getTaskUntil().isAfter(LocalDateTime.now()))
            return ResponseEntity.status(409).body(null);

        timeLockServiceFactory.create(l).clearTask();
        timeLockRepository.save(l);
        return ResponseEntity.noContent().build();
    }

    // ── Hygiene-Öffnung starten ───────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/hygiene/start")
    @Transactional
    public ResponseEntity<Map<String, Object>> startHygiene(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.getHygineOpeningEveryMinites() == null) return ResponseEntity.status(409).build();
        if (l.getTempOpeningTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_open"));

        TimeLockService service = timeLockServiceFactory.create(l);
        service.startHygieneOpening();

        return ResponseEntity.ok(Map.of(
                "unlockCode",       l.getUnlockCode() != null ? l.getUnlockCode() : "",
                "durationMinutes",  l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0,
                "openedAt",         l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : ""));
    }

    // ── Hygiene-Öffnung beenden ───────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/hygiene/end")
    @Transactional
    public ResponseEntity<Map<String, Object>> endHygiene(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.getTempOpeningTime() == null || TempOpeningReason.HYGIENE != l.getTempOpeningReason())
            return ResponseEntity.status(409).build();

        TimeLockService service = timeLockServiceFactory.create(l);
        String newCode = service.endTempOpening();

        return ResponseEntity.ok(Map.of(
                "newUnlockCode", newCode,
                "newUnlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : ""));
    }

    // ── Verifikation starten ─────────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/verification/start")
    @Transactional
    public ResponseEntity<Map<String, Object>> startVerification(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();

        var dto = timeLockServiceFactory.create(l).startVerification();
        return ResponseEntity.ok(Map.of("verificationId", dto.verficationId().toString(), "code", dto.code()));
    }

    // ── Verifikation abschließen ──────────────────────────────────────────────────

    @PostMapping(value = "/timelock/{lockId}/verification/{verificationId}/complete",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Transactional
    public ResponseEntity<Void> completeVerification(
            @PathVariable UUID lockId,
            @PathVariable UUID verificationId,
            @RequestParam MultipartFile image,
            Principal principal) throws IOException {

        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();

        boolean found = timeLockServiceFactory.create(l).completeVerification(verificationId, scaleImage(image.getBytes(), 1024));
        if (!found) return ResponseEntity.notFound().build();

        if (l.getKeyholder() != null) {
            String meName = userRepository.findById(myId).map(u -> u.getName()).orElse("");
            systemMessageService.send(myId, l.getKeyholder(),
                    "📸 " + meName + " hat eine Verifikation eingereicht.",
                    "/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
        }
        return ResponseEntity.noContent().build();
    }

    // ── Lock beenden (Zeit abgelaufen / Test-Lock) ────────────────────────────────

    @DeleteMapping("/timelock/{lockId}")
    @Transactional
    public ResponseEntity<Void> endLock(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();

        LocalDateTime now = LocalDateTime.now();
        boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
        boolean isFrozen = l.getFrozenFrom() != null
                && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));

        if (!l.isTestLock() && !timeUp && !l.isKeyholderRequestedUnlock()) {
            return ResponseEntity.status(409).build(); // Not yet unlockable
        }
        if (isFrozen && !l.isTestLock()) {
            return ResponseEntity.status(409).body(null); // Frozen
        }

        TimeLockService service = timeLockServiceFactory.create(l);
        if (l.getUnlockCode() != null && !l.getUnlockCode().isBlank()) {
            String reason = l.isKeyholderRequestedUnlock() ? "KEYHOLDER_UNLOCK" : "LOCK_OPEN";
            unlockCodeHistoryService.save(myId, lockId, l.getName(), l.getUnlockCode(), reason);
        }
        service.unlock(l.getUnlockCode());

        // Clean up verifications
        var verifications = verificationRepository.findByLockId(lockId);
        verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId()));
        verificationRepository.deleteAll(verifications);
        invitationRepository.deleteByLockId(lockId);
        timeLockRepository.deleteByLockId(lockId);

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

    // ── Tatsächliche Entsperrzeit setzen ─────────────────────────────────────────

    @PatchMapping("/timelock/{lockId}/unlock-time")
    @Transactional
    public ResponseEntity<Void> setActualUnlockTime(@PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();

        LocalDateTime now = LocalDateTime.now();
        boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
        if (!timeUp && !l.isTestLock() && !l.isKeyholderRequestedUnlock())
            return ResponseEntity.status(409).build();

        l.setUnlockTime(now);
        timeLockRepository.save(l);
        return ResponseEntity.noContent().build();
    }

    // ── Keyholder-Ansicht ─────────────────────────────────────────────────────────

    @GetMapping("/timelock/as-keyholder")
    public ResponseEntity<List<Map<String, Object>>> getTimeLocksAsKeyholder(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var locks = timeLockRepository.findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(myId);
        List<Map<String, Object>> result = new ArrayList<>();
        for (var lock : locks) {
            var lockeeOpt = userRepository.findById(lock.getLockee());
            if (lockeeOpt.isEmpty()) continue;
            var lockee = lockeeOpt.get();
            LocalDateTime now = LocalDateTime.now();
            boolean isFrozen = lock.getFrozenFrom() != null
                    && (lock.getFrozenUntil() == null || lock.getFrozenUntil().isAfter(now));
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("lockId",                  lock.getLockId().toString());
            item.put("lockType",                "TIMELOCK");
            item.put("lockName",                lock.getName() != null ? lock.getName() : "TimeLock");
            item.put("lockeeName",              lockee.getName());
            item.put("lockeeId",                lockee.getUserId().toString());
            item.put("lockeeProfilePic",        lockee.getProfilePicture());
            item.put("startTime",               lock.getStartTime() != null ? lock.getStartTime().toString() : null);
            item.put("isFrozen",                isFrozen);
            item.put("emergencyUnlockRequested", lock.getEmergencyUnlockRequestedAt() != null);
            result.add(item);
        }
        return ResponseEntity.ok(result);
    }

    @GetMapping("/timelock/as-keyholder/{lockId}")
    public ResponseEntity<Map<String, Object>> getTimeLockAsKeyholder(
            @PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();

        var lockeeOpt = userRepository.findById(l.getLockee());
        if (lockeeOpt.isEmpty()) return ResponseEntity.notFound().build();
        var lockee = lockeeOpt.get();

        LocalDateTime now = LocalDateTime.now();
        boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
        boolean isFrozen = l.getFrozenFrom() != null
                && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));

        // Verifikation
        boolean verificationDue = false;
        boolean verificationDoneToday = false;
        String verificationMyVote = null;
        String verificationTodayId = null;
        if (l.isRequiresVerification()) {
            LocalDateTime todayStart = LocalDate.now().atStartOfDay();
            LocalDateTime todayEnd   = todayStart.plusDays(1);
            var completed = verificationRepository
                    .findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId, todayStart, todayEnd);
            if (!completed.isEmpty()) {
                verificationDoneToday = true;
                var todayV = completed.get(0);
                verificationTodayId  = todayV.getDisplayId().toString();
                var myVote = verificationVoteRepository
                        .findAllByVerificationId(todayV.getDisplayId()).stream()
                        .filter(v -> myId.equals(v.getVoteId())).findFirst();
                verificationMyVote = myVote.map(v -> v.isUpvote() ? "upvote" : "downvote").orElse(null);
            } else {
                verificationDue = true;
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("lockId",                   l.getLockId().toString());
        result.put("lockType",                 "TIMELOCK");
        result.put("lockName",                 l.getName() != null ? l.getName() : "TimeLock");
        result.put("lockeeId",                 lockee.getUserId().toString());
        result.put("lockeeName",               lockee.getName());
        result.put("lockeeProfilePic",         lockee.getProfilePicture());
        result.put("startTime",                l.getStartTime() != null ? l.getStartTime().toString() : null);
        result.put("unlockTime",               (l.isEndTimeVisible() || timeUp)
                ? (l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null) : null);
        result.put("timeUp",                   timeUp);
        result.put("isFrozen",                 isFrozen);
        result.put("frozenUntil",              l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
        result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
        result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
        result.put("requiresVerification",     l.isRequiresVerification());
        result.put("verificationDue",          verificationDue);
        result.put("verificationDoneToday",    verificationDoneToday);
        result.put("verificationMyVote",       verificationMyVote);
        result.put("verificationTodayId",      verificationTodayId);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/timelock/as-keyholder/{lockId}/request-unlock")
    @Transactional
    public ResponseEntity<?> requestUnlockAsKeyholder(
            @PathVariable UUID lockId, Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();

        l.setKeyholderRequestedUnlock(true);
        timeLockRepository.save(l);
        return ResponseEntity.noContent().build();
    }

    // ── Einfrieren (Keyholder) ────────────────────────────────────────────────────

    record FreezeRequest(String frozenUntil) {}

    @PostMapping("/timelock/as-keyholder/{lockId}/freeze")
    @Transactional
    public ResponseEntity<?> freezeTimeLock(@PathVariable UUID lockId,
            @RequestBody FreezeRequest req, Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();

        LocalDateTime until;
        try {
            until = LocalDateTime.parse(req.frozenUntil());
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat."));
        }
        if (!until.isAfter(LocalDateTime.now()))
            return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen."));

        l.setFrozenFrom(LocalDateTime.now());
        l.setFrozenUntil(until);
        timeLockRepository.save(l);

        systemMessageService.send(myId, l.getLockee(),
                me.getName() + " hat dein Lock bis "
                        + until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + " "
                        + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm"))
                        + " Uhr eingefroren.",
                "/games/chastity/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);

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

    @DeleteMapping("/timelock/as-keyholder/{lockId}/freeze")
    @Transactional
    public ResponseEntity<?> unfreezeTimeLock(@PathVariable UUID lockId, Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();

        TimeLockService service = timeLockServiceFactory.create(l);
        service.unfreeze();
        l.setFrozenFrom(null);
        l.setFrozenUntil(null);
        timeLockRepository.save(l);

        systemMessageService.send(myId, l.getLockee(),
                me.getName() + " hat dein Lock entfroren.",
                "/games/chastity/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);

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

    // ── Notfall-Entsperrung ───────────────────────────────────────────────────────

    @PostMapping("/timelock/{lockId}/emergency-unlock")
    @Transactional
    public ResponseEntity<?> requestEmergencyUnlock(@PathVariable UUID lockId, Principal principal) {
        var me = userService.requireUser(principal);
        UUID myId = me.getUserId();

        var lockOpt = timeLockRepository.findById(lockId);
        if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
        var l = lockOpt.get();
        if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
        if (l.isTestLock()) return ResponseEntity.badRequest().build();
        if (l.getEmergencyUnlockRequestedAt() != null) return ResponseEntity.status(409).build();

        l.setEmergencyUnlockRequestedAt(LocalDateTime.now());
        if (l.getKeyholder() == null) {
            // Kein Keyholder: sofort entsperren, Lock als ungültig markieren (keine Historie, keine XP)
            l.setEmergencyAutoUnlocked(true);
            l.setKeyholderRequestedUnlock(true);
            timeLockRepository.save(l);
            if (l.getUnlockCode() != null && !l.getUnlockCode().isBlank()) {
                unlockCodeHistoryService.save(myId, lockId, l.getName(), l.getUnlockCode(), "EMERGENCY_UNLOCK");
            }
            return ResponseEntity.ok(Map.of("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""));
        } else {
            systemMessageService.send(myId, l.getKeyholder(),
                    "⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde.",
                    "/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY);
            timeLockRepository.save(l);
            return ResponseEntity.noContent().build();
        }
    }

    // ── Hilfsmethoden ─────────────────────────────────────────────────────────────

    private TimeLockEntity buildBaseEntity(TimeLockTemplateEntity template, UUID keyholder, UUID lockee, boolean testLock) {
        TimeLockEntity lock = new TimeLockEntity();
        lock.setName(template.getName());
        lock.setLockee(lockee);
        lock.setKeyholder(keyholder);
        lock.setRequiresVerification(template.isRequiresVerification());
        lock.setTestLock(testLock);
        lock.setEndTimeVisible(template.isEndTimeVisible());
        lock.setTasks(template.getTasks());
        lock.setTaskMode(template.getTaskCardMode());
        lock.setTaskEveryMinutes(template.getTaskEveryMinutes());
        lock.setMinTasksPerDay(template.getMinTasksPerDay());
        lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
        lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes());
        lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
        lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
        lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
        lock.setPenaltyType(template.getPenaltyType());
        lock.setPenaltyValue(template.getPenaltyValue());
        lock.setMinTimeInMinutes(template.getMinTimeInMinutes());
        lock.setMaxTimeInMinutes(template.getMaxTimeInMinutes());
        return lock;
    }


    private byte[] scaleImage(byte[] input, int maxSize) throws IOException {
        BufferedImage original = ImageIO.read(new ByteArrayInputStream(input));
        if (original == null) return input;
        int w = original.getWidth(), h = original.getHeight();
        if (w <= maxSize && h <= maxSize) return input;
        double scale = (double) maxSize / Math.max(w, h);
        int newW = (int) (w * scale), newH = (int) (h * scale);
        BufferedImage scaled = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = scaled.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(original, 0, 0, newW, newH, null);
        g.dispose();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ImageIO.write(scaled, "jpeg", out);
        return out.toByteArray();
    }

}
