package com.writeoff.security;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class LoginAttemptService {

    private static final int MAX_ATTEMPTS = 5;
    private static final long FAILURE_WINDOW_MINUTES = 30;
    private static final long LOCK_DURATION_MINUTES = 15;

    private final JdbcTemplate jdbcTemplate;

    public LoginAttemptService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional
    public LoginAttemptStatus recordFailure(String key) {
        for (int retry = 0; retry < 2; retry++) {
            try {
                return recordFailureInternal(key);
            } catch (DuplicateKeyException ex) {
                if (retry == 1) {
                    throw ex;
                }
            }
        }
        throw new IllegalStateException("Unable to record login failure");
    }

    public void clearFailures(String key) {
        jdbcTemplate.update("DELETE FROM auth_login_attempt WHERE attempt_key = ?", key);
    }

    public boolean isLocked(String key) {
        return getStatus(key).isLocked();
    }

    public long getRemainingLockSeconds(String key) {
        return getStatus(key).getRemainingLockSeconds();
    }

    public int getFailureCount(String key) {
        return getStatus(key).getFailureCount();
    }

    public LoginAttemptStatus getStatus(String key) {
        return toStatus(findRecord(key), LocalDateTime.now());
    }

    private LoginAttemptStatus recordFailureInternal(String key) {
        LocalDateTime now = LocalDateTime.now();
        AttemptRecord existing = findRecordForUpdate(key);
        if (existing == null) {
            jdbcTemplate.update(
                    "INSERT INTO auth_login_attempt (attempt_key, failure_count, window_started_at, last_failed_at, locked_until) " +
                            "VALUES (?, ?, ?, ?, ?)",
                    key,
                    1,
                    Timestamp.valueOf(now),
                    Timestamp.valueOf(now),
                    null
            );
            return new LoginAttemptStatus(1, 0);
        }

        LoginAttemptStatus current = toStatus(existing, now);
        if (current.isLocked()) {
            return current;
        }

        boolean windowExpired = isWindowExpired(existing.getWindowStartedAt(), now);
        int nextFailureCount = windowExpired ? 1 : existing.getFailureCount() + 1;
        LocalDateTime nextWindowStartedAt = windowExpired ? now : existing.getWindowStartedAt();
        LocalDateTime nextLockedUntil = nextFailureCount >= MAX_ATTEMPTS ? now.plusMinutes(LOCK_DURATION_MINUTES) : null;

        jdbcTemplate.update(
                "UPDATE auth_login_attempt " +
                        "SET failure_count = ?, window_started_at = ?, last_failed_at = ?, locked_until = ?, updated_at = CURRENT_TIMESTAMP " +
                        "WHERE attempt_key = ?",
                nextFailureCount,
                Timestamp.valueOf(nextWindowStartedAt),
                Timestamp.valueOf(now),
                nextLockedUntil == null ? null : Timestamp.valueOf(nextLockedUntil),
                key
        );

        return toStatus(new AttemptRecord(nextFailureCount, nextWindowStartedAt, nextLockedUntil), now);
    }

    private AttemptRecord findRecord(String key) {
        List<AttemptRecord> records = jdbcTemplate.query(
                "SELECT failure_count, window_started_at, locked_until FROM auth_login_attempt WHERE attempt_key = ?",
                (rs, rowNum) -> mapAttemptRecord(rs),
                key
        );
        return records.isEmpty() ? null : records.get(0);
    }

    private AttemptRecord findRecordForUpdate(String key) {
        List<AttemptRecord> records = jdbcTemplate.query(
                "SELECT failure_count, window_started_at, locked_until FROM auth_login_attempt WHERE attempt_key = ? FOR UPDATE",
                (rs, rowNum) -> mapAttemptRecord(rs),
                key
        );
        return records.isEmpty() ? null : records.get(0);
    }

    private AttemptRecord mapAttemptRecord(ResultSet rs) throws SQLException {
        Timestamp windowStartedAt = rs.getTimestamp("window_started_at");
        Timestamp lockedUntil = rs.getTimestamp("locked_until");
        return new AttemptRecord(
                rs.getInt("failure_count"),
                windowStartedAt == null ? null : windowStartedAt.toLocalDateTime(),
                lockedUntil == null ? null : lockedUntil.toLocalDateTime()
        );
    }

    private LoginAttemptStatus toStatus(AttemptRecord record, LocalDateTime now) {
        if (record == null) {
            return new LoginAttemptStatus(0, 0);
        }
        int failureCount = isWindowExpired(record.getWindowStartedAt(), now) ? 0 : record.getFailureCount();
        long remainingLockSeconds = 0;
        if (record.getLockedUntil() != null && record.getLockedUntil().isAfter(now)) {
            remainingLockSeconds = Math.max(1, Duration.between(now, record.getLockedUntil()).getSeconds());
        }
        return new LoginAttemptStatus(failureCount, remainingLockSeconds);
    }

    private boolean isWindowExpired(LocalDateTime windowStartedAt, LocalDateTime now) {
        return windowStartedAt == null || !now.isBefore(windowStartedAt.plusMinutes(FAILURE_WINDOW_MINUTES));
    }

    public static class LoginAttemptStatus {
        private final int failureCount;
        private final long remainingLockSeconds;

        LoginAttemptStatus(int failureCount, long remainingLockSeconds) {
            this.failureCount = failureCount;
            this.remainingLockSeconds = remainingLockSeconds;
        }

        public int getFailureCount() {
            return failureCount;
        }

        public long getRemainingLockSeconds() {
            return remainingLockSeconds;
        }

        public boolean isLocked() {
            return remainingLockSeconds > 0;
        }

        public int getRemainingAttempts() {
            return Math.max(0, MAX_ATTEMPTS - failureCount);
        }
    }

    private static class AttemptRecord {
        private final int failureCount;
        private final LocalDateTime windowStartedAt;
        private final LocalDateTime lockedUntil;

        AttemptRecord(int failureCount, LocalDateTime windowStartedAt, LocalDateTime lockedUntil) {
            this.failureCount = failureCount;
            this.windowStartedAt = windowStartedAt;
            this.lockedUntil = lockedUntil;
        }

        int getFailureCount() {
            return failureCount;
        }

        LocalDateTime getWindowStartedAt() {
            return windowStartedAt;
        }

        LocalDateTime getLockedUntil() {
            return lockedUntil;
        }
    }
}
