package com.writeoff.module.auth.service;

import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.security.AuthScope;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
public class RefreshTokenService {
    private final JdbcTemplate jdbcTemplate;
    private final SecureRandom secureRandom = new SecureRandom();
    private final long refreshExpireDays;
    private final long idleTimeoutMinutes;

    public RefreshTokenService(JdbcTemplate jdbcTemplate,
                               @Value("${app.security.refresh-expire-days:14}") long refreshExpireDays,
                               @Value("${app.security.idle-timeout-minutes:60}") long idleTimeoutMinutes) {
        this.jdbcTemplate = jdbcTemplate;
        this.refreshExpireDays = refreshExpireDays <= 0 ? 14 : refreshExpireDays;
        this.idleTimeoutMinutes = idleTimeoutMinutes <= 0 ? 60 : idleTimeoutMinutes;
    }

    @Transactional(rollbackFor = Exception.class)
    public String issue(Long userId, Long tenantId, AuthScope scope, String ip, String userAgent) {
        return String.valueOf(issueWithSession(userId, tenantId, scope, ip, userAgent).get("refreshToken"));
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> issueWithSession(Long userId, Long tenantId, AuthScope scope, String ip, String userAgent) {
        String rawToken = generateToken();
        String tokenHash = sha256Hex(rawToken);
        Long safeUser = userId == null ? 0L : userId;
        jdbcTemplate.update(
                "INSERT INTO auth_refresh_token (user_id, tenant_id, scope, token_hash, status, expires_at, ip_hash, ua_hash, created_by, updated_by) " +
                        "VALUES (?, ?, ?, ?, 'ACTIVE', DATE_ADD(NOW(), INTERVAL ? DAY), ?, ?, ?, ?)",
                safeUser,
                tenantId,
                scope.name(),
                tokenHash,
                refreshExpireDays,
                sha256Hex(safe(ip)),
                sha256Hex(safe(userAgent)),
                safeUser,
                safeUser
        );
        Long sessionId = jdbcTemplate.queryForObject(
                "SELECT id FROM auth_refresh_token WHERE token_hash=? LIMIT 1",
                Long.class,
                tokenHash
        );
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("sessionId", sessionId);
        data.put("refreshToken", rawToken);
        return data;
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> rotate(String rawToken, String ip, String userAgent) {
        if (rawToken == null || rawToken.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.REFRESH_TOKEN_INVALID, "刷新会话无效");
        }
        String tokenHash = sha256Hex(rawToken.trim());
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, user_id, tenant_id, scope, status, issued_at, expires_at, last_used_at, is_deleted " +
                        "FROM auth_refresh_token WHERE token_hash=? LIMIT 1",
                tokenHash
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.REFRESH_TOKEN_INVALID, "刷新会话无效");
        }
        Map<String, Object> row = rows.get(0);
        String status = String.valueOf(row.get("status"));
        int isDeleted = toFlagInt(row.get("is_deleted"));
        if (isDeleted == 1 || !"ACTIVE".equals(status)) {
            throw new BusinessException(ErrorCodes.REFRESH_TOKEN_INVALID, "刷新会话无效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime lastActivityAt = resolveLastActivityAt(row.get("last_used_at"), row.get("issued_at"));
        if (lastActivityAt == null || now.isAfter(lastActivityAt.plusMinutes(idleTimeoutMinutes))) {
            Long currentId = ((Number) row.get("id")).longValue();
            jdbcTemplate.update(
                    "UPDATE auth_refresh_token SET status='REVOKED', revoked_at=CURRENT_TIMESTAMP, revoked_reason='IDLE_TIMEOUT', updated_at=CURRENT_TIMESTAMP WHERE id=? AND status='ACTIVE'",
                    currentId
            );
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "浼氳瘽澶辨晥");
        }
        LocalDateTime expiresAt = toLocalDateTime(row.get("expires_at"));
        if (expiresAt == null || now.isAfter(expiresAt)) {
            Long currentId = ((Number) row.get("id")).longValue();
            jdbcTemplate.update(
                    "UPDATE auth_refresh_token SET status='EXPIRED', revoked_at=CURRENT_TIMESTAMP, revoked_reason='EXPIRED', updated_at=CURRENT_TIMESTAMP WHERE id=?",
                    currentId
            );
            throw new BusinessException(ErrorCodes.REFRESH_TOKEN_EXPIRED, "刷新会话已过期");
        }

        Long currentId = ((Number) row.get("id")).longValue();
        Long userId = ((Number) row.get("user_id")).longValue();
        Long tenantId = row.get("tenant_id") == null ? null : ((Number) row.get("tenant_id")).longValue();
        AuthScope scope = AuthScope.fromClaim(String.valueOf(row.get("scope")));

        String nextRawToken = generateToken();
        String nextTokenHash = sha256Hex(nextRawToken);
        jdbcTemplate.update(
                "INSERT INTO auth_refresh_token (user_id, tenant_id, scope, token_hash, status, expires_at, ip_hash, ua_hash, created_by, updated_by) " +
                        "VALUES (?, ?, ?, ?, 'ACTIVE', DATE_ADD(NOW(), INTERVAL ? DAY), ?, ?, ?, ?)",
                userId,
                tenantId,
                scope.name(),
                nextTokenHash,
                refreshExpireDays,
                sha256Hex(safe(ip)),
                sha256Hex(safe(userAgent)),
                userId,
                userId
        );
        Long nextId = jdbcTemplate.queryForObject(
                "SELECT id FROM auth_refresh_token WHERE token_hash=? LIMIT 1",
                Long.class,
                nextTokenHash
        );
        jdbcTemplate.update(
                "UPDATE auth_refresh_token SET status='ROTATED', rotated_to_id=?, last_used_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?",
                nextId,
                currentId
        );
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("userId", userId);
        data.put("tenantId", tenantId);
        data.put("scope", scope);
        data.put("refreshToken", nextRawToken);
        data.put("sessionId", nextId);
        return data;
    }

    @Transactional(rollbackFor = Exception.class)
    public void revokeCurrent(String rawToken, String reason) {
        if (rawToken == null || rawToken.trim().isEmpty()) {
            return;
        }
        String tokenHash = sha256Hex(rawToken.trim());
        jdbcTemplate.update(
                "UPDATE auth_refresh_token SET status='REVOKED', revoked_at=CURRENT_TIMESTAMP, revoked_reason=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE token_hash=? AND status='ACTIVE'",
                safe(reason),
                tokenHash
        );
    }

    @Transactional(rollbackFor = Exception.class)
    public void revokeAllByPrincipal(Long userId, Long tenantId, AuthScope scope, String reason) {
        if (userId == null || scope == null) {
            return;
        }
        if (scope == AuthScope.TENANT) {
            jdbcTemplate.update(
                    "UPDATE auth_refresh_token SET status='REVOKED', revoked_at=CURRENT_TIMESTAMP, revoked_reason=?, updated_at=CURRENT_TIMESTAMP " +
                            "WHERE user_id=? AND tenant_id=? AND scope=? AND status='ACTIVE'",
                    safe(reason),
                    userId,
                    tenantId,
                    scope.name()
            );
            return;
        }
        jdbcTemplate.update(
                "UPDATE auth_refresh_token SET status='REVOKED', revoked_at=CURRENT_TIMESTAMP, revoked_reason=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE user_id=? AND tenant_id IS NULL AND scope=? AND status='ACTIVE'",
                safe(reason),
                userId,
                scope.name()
        );
    }

    private String generateToken() {
        byte[] bytes = new byte[48];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    private String sha256Hex(String value) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(value.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(hash.length * 2);
            for (byte b : hash) {
                String h = Integer.toHexString(b & 0xff);
                if (h.length() == 1) {
                    sb.append('0');
                }
                sb.append(h);
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 unavailable", e);
        }
    }

    private String safe(String value) {
        return value == null ? "" : value.trim();
    }

    private int toFlagInt(Object value) {
        if (value == null) {
            return 0;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof Boolean) {
            return ((Boolean) value) ? 1 : 0;
        }
        String text = String.valueOf(value).trim();
        if ("true".equalsIgnoreCase(text)) {
            return 1;
        }
        if ("false".equalsIgnoreCase(text)) {
            return 0;
        }
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException ex) {
            return 0;
        }
    }

    private LocalDateTime toLocalDateTime(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof java.util.Date) {
            return new java.sql.Timestamp(((java.util.Date) value).getTime()).toLocalDateTime();
        }
        if (value instanceof String) {
            String text = String.valueOf(value).trim();
            if (text.isEmpty()) {
                return null;
            }
            try {
                return LocalDateTime.parse(text.replace(' ', 'T'));
            } catch (Exception ignored) {
            }
        }
        return null;
    }

    private LocalDateTime resolveLastActivityAt(Object lastUsedAt, Object issuedAt) {
        LocalDateTime lastUsed = toLocalDateTime(lastUsedAt);
        if (lastUsed != null) {
            return lastUsed;
        }
        return toLocalDateTime(issuedAt);
    }
}
