package com.writeoff.security;

import com.writeoff.common.exception.BusinessException;
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.net.URLEncoder;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
public class PasswordSetupService {
    private static final String SCENARIO_TENANT_ADMIN_SETUP = "TENANT_ADMIN_SETUP";
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final JdbcTemplate jdbcTemplate;
    private final PasswordPolicyService passwordPolicyService;
    private final PasswordCodecService passwordCodecService;
    private final String frontendBaseUrl;
    private final long passwordSetupExpireMinutes;
    private final SecureRandom secureRandom = new SecureRandom();

    public PasswordSetupService(JdbcTemplate jdbcTemplate,
                                PasswordPolicyService passwordPolicyService,
                                PasswordCodecService passwordCodecService,
                                @Value("${app.frontend-base-url:http://localhost:5173}") String frontendBaseUrl,
                                @Value("${app.security.password-setup-expire-minutes:1440}") long passwordSetupExpireMinutes) {
        this.jdbcTemplate = jdbcTemplate;
        this.passwordPolicyService = passwordPolicyService;
        this.passwordCodecService = passwordCodecService;
        this.frontendBaseUrl = frontendBaseUrl;
        this.passwordSetupExpireMinutes = passwordSetupExpireMinutes;
    }

    @Transactional
    public String issueTenantAdminSetupLink(Long tenantId, Long userId, Long operatorUserId) {
        Map<String, Object> user = loadTenantAdminUser(tenantId, userId);
        String tenantCode = String.valueOf(user.get("tenant_code"));
        jdbcTemplate.update(
                "UPDATE auth_password_setup_token " +
                        "SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE tenant_id=? AND user_id=? AND scenario=? AND is_deleted=0 AND used_at IS NULL",
                safeOperator(operatorUserId),
                tenantId,
                userId,
                SCENARIO_TENANT_ADMIN_SETUP
        );

        String rawToken = generateToken();
        LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(Math.max(passwordSetupExpireMinutes, 10L));
        jdbcTemplate.update(
                "INSERT INTO auth_password_setup_token (tenant_id, user_id, scenario, token_hash, expires_at, created_by, updated_by) " +
                        "VALUES (?, ?, ?, ?, ?, ?, ?)",
                tenantId,
                userId,
                SCENARIO_TENANT_ADMIN_SETUP,
                hashToken(rawToken),
                Timestamp.valueOf(expiresAt),
                safeOperator(operatorUserId),
                safeOperator(operatorUserId)
        );
        return buildSetupLink(tenantCode, rawToken);
    }

    public Map<String, Object> verifyTenantPasswordSetupToken(String tenantCode, String rawToken) {
        Map<String, Object> tokenRecord = loadAvailableTokenRecord(tenantCode, rawToken);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("tenantCode", tokenRecord.get("tenant_code"));
        data.put("tenantName", tokenRecord.get("tenant_name"));
        data.put("userName", tokenRecord.get("user_name"));
        data.put("phone", maskPhone(tokenRecord.get("phone")));
        data.put("expiresAt", formatDateTime(tokenRecord.get("expires_at")));
        return data;
    }

    @Transactional
    public Map<String, Object> completeTenantPasswordSetup(String tenantCode, String rawToken, String newPassword) {
        passwordPolicyService.validate(newPassword);
        Map<String, Object> tokenRecord = loadAvailableTokenRecord(tenantCode, rawToken);
        Long tokenId = ((Number) tokenRecord.get("id")).longValue();
        Long tenantId = ((Number) tokenRecord.get("tenant_id")).longValue();
        Long userId = ((Number) tokenRecord.get("user_id")).longValue();

        int consumed = jdbcTemplate.update(
                "UPDATE auth_password_setup_token " +
                        "SET used_at=CURRENT_TIMESTAMP, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE id=? AND used_at IS NULL AND is_deleted=0 AND expires_at>=CURRENT_TIMESTAMP",
                userId,
                tokenId
        );
        if (consumed <= 0) {
            throw new BusinessException(10001, "设置链接无效或已过期");
        }

        jdbcTemplate.update(
                "UPDATE sys_user SET password_hash=?, status='ENABLED', updated_by=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE tenant_id=? AND id=? AND is_deleted=0",
                passwordCodecService.encode(newPassword),
                userId,
                tenantId,
                userId
        );
        jdbcTemplate.update(
                "UPDATE auth_password_setup_token " +
                        "SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE tenant_id=? AND user_id=? AND scenario=? AND id<>? AND is_deleted=0 AND used_at IS NULL",
                userId,
                tenantId,
                userId,
                SCENARIO_TENANT_ADMIN_SETUP,
                tokenId
        );

        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("ok", Boolean.TRUE);
        data.put("tenantCode", tokenRecord.get("tenant_code"));
        return data;
    }

    private Map<String, Object> loadAvailableTokenRecord(String tenantCode, String rawToken) {
        String normalizedTenantCode = normalizeText(tenantCode);
        String normalizedToken = normalizeText(rawToken);
        if (normalizedTenantCode.isEmpty() || normalizedToken.isEmpty()) {
            throw new BusinessException(10001, "设置链接无效或已过期");
        }

        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT tkn.id, tkn.tenant_id, tkn.user_id, tkn.expires_at, tkn.used_at, " +
                        "t.tenant_code, t.tenant_name, u.user_name, u.phone " +
                        "FROM auth_password_setup_token tkn " +
                        "JOIN tenant t ON tkn.tenant_id=t.id " +
                        "JOIN sys_user u ON tkn.user_id=u.id AND tkn.tenant_id=u.tenant_id " +
                        "WHERE tkn.token_hash=? AND tkn.scenario=? AND tkn.is_deleted=0 " +
                        "AND t.is_deleted=0 AND u.is_deleted=0 " +
                        "LIMIT 1",
                hashToken(normalizedToken),
                SCENARIO_TENANT_ADMIN_SETUP
        );
        if (rows.isEmpty()) {
            throw new BusinessException(10001, "设置链接无效或已过期");
        }

        Map<String, Object> row = rows.get(0);
        String rowTenantCode = normalizeText(row.get("tenant_code"));
        if (!normalizedTenantCode.equalsIgnoreCase(rowTenantCode)) {
            throw new BusinessException(10001, "设置链接无效或已过期");
        }
        if (row.get("used_at") != null) {
            throw new BusinessException(10001, "设置链接已失效，请联系平台重新发送");
        }

        LocalDateTime expiresAt = toLocalDateTime(row.get("expires_at"));
        if (expiresAt == null || LocalDateTime.now().isAfter(expiresAt)) {
            throw new BusinessException(10001, "设置链接无效或已过期");
        }
        return row;
    }

    private Map<String, Object> loadTenantAdminUser(Long tenantId, Long userId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.id, u.tenant_id, t.tenant_code " +
                        "FROM sys_user u " +
                        "JOIN tenant t ON u.tenant_id=t.id " +
                        "WHERE u.tenant_id=? AND u.id=? AND u.is_deleted=0 AND t.is_deleted=0 " +
                        "LIMIT 1",
                tenantId,
                userId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(10003, "租户管理员不存在");
        }
        return rows.get(0);
    }

    private String buildSetupLink(String tenantCode, String rawToken) {
        String baseUrl = normalizeBaseUrl(frontendBaseUrl);
        String path = "/" + tenantCode + "/setup-password?token=" + urlEncode(rawToken);
        return baseUrl.isEmpty() ? path : baseUrl + path;
    }

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

    private String hashToken(String rawToken) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(rawToken.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : hash) {
                String hex = Integer.toHexString(item & 0xff);
                if (hex.length() == 1) {
                    sb.append('0');
                }
                sb.append(hex);
            }
            return sb.toString();
        } catch (Exception ex) {
            throw new IllegalStateException("Failed to hash password setup token", ex);
        }
    }

    private String normalizeBaseUrl(String rawBaseUrl) {
        String value = normalizeText(rawBaseUrl);
        while (value.endsWith("/")) {
            value = value.substring(0, value.length() - 1);
        }
        return value;
    }

    private String urlEncode(String raw) {
        try {
            return URLEncoder.encode(raw, "UTF-8");
        } catch (Exception ex) {
            throw new IllegalStateException("Failed to encode password setup link", ex);
        }
    }

    private String normalizeText(Object raw) {
        return raw == null ? "" : String.valueOf(raw).trim();
    }

    private String formatDateTime(Object value) {
        LocalDateTime dateTime = toLocalDateTime(value);
        return dateTime == null ? "" : dateTime.format(DATE_TIME_FORMATTER);
    }

    private LocalDateTime toLocalDateTime(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof Timestamp) {
            return ((Timestamp) value).toLocalDateTime();
        }
        if (value instanceof java.util.Date) {
            return new 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;
            }
        }
        return null;
    }

    private String maskPhone(Object rawPhone) {
        String phone = normalizeText(rawPhone);
        if (phone.length() < 7) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }

    private Long safeOperator(Long operatorUserId) {
        return operatorUserId == null ? 0L : operatorUserId;
    }
}
