package io.github.shirainbown.sharelink.download;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import io.github.shirainbown.sharelink.SharelinkSettingConfigGetter;
import io.github.shirainbown.sharelink.emailcode.EmailCodeManager;
import io.github.shirainbown.sharelink.model.DownloadResource;
import io.github.shirainbown.sharelink.util.EmailUtils;
import io.github.shirainbown.sharelink.util.IpUtils;
import io.github.shirainbown.sharelink.verify.VerifiedEmailService;

/**
 * Public endpoint exchanging a (possibly verified) identity for a one-time download
 * token. Mounted under {@code /apis/api.sharelink.halo.run/v1alpha1} and opened to
 * anonymous visitors via the aggregate-to-anonymous role template.
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class DownloadTokenEndpoint implements CustomEndpoint {

    private final ReactiveExtensionClient client;
    private final SharelinkSettingConfigGetter settingConfigGetter;
    private final VerifiedEmailService verifiedEmailService;
    private final EmailCodeManager emailCodeManager;
    private final DownloadTokenManager tokenManager;

    @Override
    public RouterFunction<ServerResponse> endpoint() {
        return RouterFunctions.route()
            .POST("downloads/-/token", this::issueToken)
            .build();
    }

    private Mono<ServerResponse> issueToken(ServerRequest request) {
        return request.bodyToMono(TokenRequest.class)
            .switchIfEmpty(Mono.error(new ServerWebInputException("请求体不能为空")))
            .flatMap(body -> {
                var slug = StringUtils.trimToNull(body.slug());
                if (slug == null) {
                    return Mono.error(new ServerWebInputException("slug 不能为空"));
                }
                return findBySlug(slug)
                    .switchIfEmpty(Mono.error(new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "下载资源不存在")))
                    .flatMap(resource -> doIssueToken(request, resource, body));
            });
    }

    private Mono<ServerResponse> doIssueToken(ServerRequest request,
                                              DownloadResource resource,
                                              TokenRequest body) {
        var spec = resource.getSpec();
        if (!spec.isEnabled()) {
            return Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN, "该资源已停用"));
        }
        var clientIp = IpUtils.clientIp(request.exchange().getRequest());
        return settingConfigGetter.getBasicConfig()
            .flatMap(basic -> {
                if (!spec.isRequireEmailVerify()) {
                    return respondWithToken(spec, null, clientIp, basic);
                }
                var email = EmailUtils.normalizeEmail(body.email());
                if (email == null || !EmailUtils.isValidEmail(email)) {
                    return Mono.error(new ServerWebInputException("邮箱格式不正确"));
                }
                return verifiedEmailService.isVerified(email)
                    .onErrorResume(e -> {
                        log.warn("查询邮箱验证状态失败，按未验证处理: {}", email, e);
                        return Mono.just(false);
                    })
                    .flatMap(verified -> {
                        if (verified) {
                            return respondWithToken(spec, email, clientIp, basic);
                        }
                        var code = StringUtils.trimToNull(body.code());
                        if (code == null) {
                            return Mono.error(new ResponseStatusException(
                                HttpStatus.FORBIDDEN, "请先完成邮箱验证"));
                        }
                        return settingConfigGetter.getEmailVerifyConfig()
                            .flatMap(config -> emailCodeManager.verify(email, code, config)
                                .flatMap(valid -> {
                                    if (!valid) {
                                        return Mono.error(new ResponseStatusException(
                                            HttpStatus.FORBIDDEN, "验证码错误或已过期，请重新获取"));
                                    }
                                    return verifiedEmailService.recordVerified(email, clientIp)
                                        .onErrorResume(e -> {
                                            log.warn("登记已验证邮箱失败: {}", email, e);
                                            return Mono.empty();
                                        })
                                        .then(respondWithToken(spec, email, clientIp, basic));
                                }));
                    });
            });
    }

    private Mono<ServerResponse> respondWithToken(DownloadResource.Spec spec, String email,
                                                  String clientIp,
                                                  SharelinkSettingConfigGetter.BasicConfig basic) {
        var token = tokenManager.issue(spec.getSlug(), email, clientIp);
        var fileUrl = "/download/" + spec.getSlug() + "/file?token=" + token;
        return ServerResponse.ok().bodyValue(new TokenResponse(token, fileUrl));
    }

    private Mono<DownloadResource> findBySlug(String slug) {
        var options = ListOptions.builder()
            .fieldQuery(QueryFactory.equal("spec.slug", slug))
            .build();
        return client.listAll(DownloadResource.class, options, Sort.unsorted()).next();
    }

    @Override
    public GroupVersion groupVersion() {
        return GroupVersion.parseAPIVersion("api.sharelink.halo.run/v1alpha1");
    }

    public record TokenRequest(String slug, String email, String code) {
    }

    public record TokenResponse(String token, String fileUrl) {
    }
}
