package tech.easyflow.auth.service.impl;

import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.tenant.TenantManager;
import org.springframework.stereotype.Service;
import tech.easyflow.auth.entity.LoginDTO;
import tech.easyflow.auth.entity.LoginVO;
import tech.easyflow.auth.service.AuthService;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.config.AccountSecurityProperties;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.entity.SysMenu;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysApiKeyService;
import tech.easyflow.system.service.SysMenuService;
import tech.easyflow.system.service.SysRoleService;
import tech.easyflow.system.util.SysPasswordPolicy;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 账号认证、会话创建与凭证更新服务。
 */
@Service
public class AuthServiceImpl implements AuthService, StpInterface {

    private static final String WEB_LOGIN_LOCK_KEY_PREFIX = "easyflow:lock:auth:web-login:";
    private static final Duration WEB_LOGIN_LOCK_WAIT = Duration.ofSeconds(3);
    private static final Duration WEB_LOGIN_LOCK_LEASE = Duration.ofSeconds(10);

    @Resource
    private SysAccountService sysAccountService;
    @Resource
    private SysRoleService sysRoleService;
    @Resource
    private SysMenuService sysMenuService;
    @Resource
    private SysApiKeyService sysApiKeyService;
    @Resource
    private RedisLockExecutor redisLockExecutor;
    @Resource
    private AccountSecurityProperties accountSecurityProperties;

    @Override
    public LoginVO login(LoginDTO loginDTO) {
        try {
            TenantManager.ignoreTenantCondition();
            String pwd = loginDTO.getPassword();
            SysAccount record = getAvailableAccount(loginDTO.getAccount(), "用户名/密码错误");
            String pwdDb = record.getPassword();
            if (!BCrypt.checkpw(pwd, pwdDb)) {
                throw new BusinessException("用户名/密码错误");
            }
            return createWebLoginVO(record);
        } finally {
            TenantManager.restoreTenantCondition();
        }
    }

    @Override
    public LoginVO devLogin(String account) {
        try {
            TenantManager.ignoreTenantCondition();
            SysAccount record = getAvailableAccount(account, "开发免登账号不存在");
            return createWebLoginVO(record);
        } finally {
            TenantManager.restoreTenantCondition();
        }
    }

    @Override
    public LoginVO loginByApiKey(String apiKey) {
        try {
            TenantManager.ignoreTenantCondition();
            sysApiKeyService.checkApikeyPermission(apiKey, "/public-api/bot/chat");
            SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
            BigInteger accountId = sysApiKey.getCreatedBy();
            if (accountId == null) {
                throw new BusinessException("访问令牌未绑定创建用户");
            }
            Long timeoutSeconds = resolveApiKeyLoginTimeoutSeconds(sysApiKey);
            return loginByAccountId(accountId, timeoutSeconds);
        } finally {
            TenantManager.restoreTenantCondition();
        }
    }

    @Override
    public LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds) {
        SysAccount record = getAvailableAccount(accountId, "账号不存在或不可用");
        return createApiKeyLoginVO(record, timeoutSeconds);
    }

    /**
     * 修改当前账号密码，并按数据库中的强制重置状态决定是否校验当前密码。
     *
     * @param accountId 账号 ID
     * @param currentPassword 当前密码；强制重置密码时可为空
     * @param newPassword 新密码
     * @param confirmPassword 确认密码
     * @param loginDevice 当前登录会话设备类型
     * @throws BusinessException 账号不存在、凭证不正确、会话来源不可信或密码更新失败时抛出
     */
    @Override
    public void updateOwnPassword(
            BigInteger accountId,
            String currentPassword,
            String newPassword,
            String confirmPassword,
            String loginDevice
    ) {
        SysAccount record = sysAccountService.getById(accountId);
        if (record == null) {
            throw new BusinessException(404, 1, "账号不存在");
        }

        boolean passwordResetRequired = Boolean.TRUE.equals(record.getPasswordResetRequired());
        if (passwordResetRequired) {
            if (!AuthLoginSessionPolicy.WEB_DEVICE.equals(loginDevice)) {
                throw new BusinessException(403, 3, "请使用账号密码登录后重置密码");
            }
        } else if (currentPassword == null
                || record.getPassword() == null
                || !BCrypt.checkpw(currentPassword, record.getPassword())) {
            throw new BusinessException(400, 1, "密码不正确");
        }

        if (!Objects.equals(newPassword, confirmPassword)) {
            throw new BusinessException(400, 2, "两次密码不一致");
        }
        SysPasswordPolicy.validateStrongPassword(newPassword);

        SysAccount update = new SysAccount();
        update.setId(accountId);
        update.setPassword(BCrypt.hashpw(newPassword));
        update.setPasswordResetRequired(false);
        update.setModified(new Date());
        update.setModifiedBy(accountId);
        if (!sysAccountService.updateById(update)) {
            throw new BusinessException(500, 4, "密码更新失败");
        }
    }

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));
        return menus.stream()
                .map(SysMenu::getPermissionTag)
                .distinct()
                .collect(Collectors.toList());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        List<SysRole> roles = sysRoleService.getRolesByAccountId(BigInteger.valueOf(Long.parseLong(loginId.toString())));
        return roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
    }

    /**
     * 根据账号安全策略创建 Web 登录会话。
     *
     * @param record 登录账号
     * @return 登录结果
     */
    private LoginVO createWebLoginVO(SysAccount record) {
        if (!AuthLoginSessionPolicy.shouldReplaceExistingWebSession(accountSecurityProperties.isMultiLogin())) {
            return createLoginVO(record, AuthLoginSessionPolicy.webLoginModel());
        }
        return redisLockExecutor.executeWithLock(
                WEB_LOGIN_LOCK_KEY_PREFIX + record.getId(),
                WEB_LOGIN_LOCK_WAIT,
                WEB_LOGIN_LOCK_LEASE,
                () -> {
                    StpUtil.replaced(record.getId(), AuthLoginSessionPolicy.WEB_DEVICE);
                    // 首次升级后的新登录同时淘汰旧版本未标记设备类型的浏览器会话。
                    StpUtil.replaced(record.getId(), AuthLoginSessionPolicy.LEGACY_WEB_DEVICE);
                    return createLoginVO(record, AuthLoginSessionPolicy.webLoginModel());
                }
        );
    }

    /**
     * 创建独立的 API Key 机器会话。
     *
     * @param record 登录账号
     * @param timeoutSeconds 会话有效期秒数
     * @return 登录结果
     */
    private LoginVO createApiKeyLoginVO(SysAccount record, Long timeoutSeconds) {
        return createLoginVO(record, AuthLoginSessionPolicy.apiKeyLoginModel(timeoutSeconds));
    }

    /**
     * 创建指定来源的登录会话。
     *
     * @param record 登录账号
     * @param loginModel 登录参数
     * @return 登录结果
     */
    private LoginVO createLoginVO(SysAccount record, SaLoginModel loginModel) {
        StpUtil.login(record.getId(), loginModel);
        LoginAccount loginAccount = new LoginAccount();
        BeanUtil.copyProperties(record, loginAccount);
        StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);

        LoginVO res = new LoginVO();
        res.setToken(StpUtil.getTokenValue());
        res.setNickname(record.getNickname());
        res.setAvatar(record.getAvatar());
        res.setForceChangePassword(Boolean.TRUE.equals(record.getPasswordResetRequired()));
        return res;
    }

    private Long resolveApiKeyLoginTimeoutSeconds(SysApiKey sysApiKey) {
        Date expiredAt = sysApiKey.getExpiredAt();
        if (expiredAt == null) {
            return null;
        }
        long remainingMs = expiredAt.getTime() - System.currentTimeMillis();
        if (remainingMs <= 0) {
            throw new BusinessException("apiKey 已过期");
        }
        long timeoutSeconds = (remainingMs + 999) / 1000;
        return Math.max(timeoutSeconds, 1L);
    }

    private SysAccount getAvailableAccount(String account, String accountNotFoundMessage) {
        QueryWrapper w = QueryWrapper.create();
        w.eq(SysAccount::getLoginName, account);
        SysAccount record = sysAccountService.getOne(w);
        return validateAvailableAccount(record, accountNotFoundMessage);
    }

    private SysAccount getAvailableAccount(BigInteger accountId, String accountNotFoundMessage) {
        SysAccount record = sysAccountService.getById(accountId);
        return validateAvailableAccount(record, accountNotFoundMessage);
    }

    private SysAccount validateAvailableAccount(SysAccount record, String accountNotFoundMessage) {
        if (record == null) {
            throw new BusinessException(accountNotFoundMessage);
        }
        if (EnumDataStatus.UNAVAILABLE.getCode().equals(record.getStatus())) {
            throw new BusinessException("账号未启用，请联系管理员");
        }
        return record;
    }
}
