package com.magistr.app.config.auth;

import com.magistr.app.model.Role;
import com.magistr.app.model.User;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

@Service
public class JwtTokenService {

    private static final String ISSUER = "magistr";

    private final JwtProperties properties;
    private final JwtEncoder encoder;
    private final JwtDecoder decoder;

    public JwtTokenService(JwtProperties properties) {
        this.properties = properties;
        SecretKey secretKey = new SecretKeySpec(properties.secretBytes(), "HmacSHA256");
        this.encoder = new NimbusJwtEncoder(new ImmutableSecret<SecurityContext>(secretKey));
        this.decoder = NimbusJwtDecoder
                .withSecretKey(secretKey)
                .macAlgorithm(MacAlgorithm.HS256)
                .build();
    }

    public String createAccessToken(User user, String tenant) {
        return createAccessToken(new AuthenticatedUser(
                user.getId(),
                user.getUsername(),
                user.getRole(),
                user.getDepartmentId()
        ), tenant);
    }

    public String createAccessToken(AuthenticatedUser user, String tenant) {
        Instant now = Instant.now();
        JwtClaimsSet.Builder claims = JwtClaimsSet.builder()
                .issuer(ISSUER)
                .issuedAt(now)
                .expiresAt(now.plus(properties.getAccessTtl()))
                .subject(String.valueOf(user.id()))
                .id(UUID.randomUUID().toString())
                .claim("tenant", tenant)
                .claim("userId", user.id())
                .claim("username", user.username())
                .claim("role", user.role().name());

        if (user.departmentId() != null) {
            claims.claim("departmentId", user.departmentId());
        }

        JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build();
        return encoder.encode(JwtEncoderParameters.from(header, claims.build())).getTokenValue();
    }

    public Optional<AuthenticatedUser> authenticate(String token, String expectedTenant) {
        if (token == null || token.isBlank() || expectedTenant == null || expectedTenant.isBlank()) {
            return Optional.empty();
        }

        try {
            Jwt jwt = decoder.decode(token);
            String tenant = jwt.getClaimAsString("tenant");
            if (!expectedTenant.equalsIgnoreCase(tenant)) {
                return Optional.empty();
            }

            Long userId = asLong(jwt.getClaim("userId")).orElseGet(() -> Long.valueOf(jwt.getSubject()));
            String username = jwt.getClaimAsString("username");
            Role role = Role.valueOf(jwt.getClaimAsString("role"));
            Long departmentId = asLong(jwt.getClaim("departmentId")).orElse(null);

            return Optional.of(new AuthenticatedUser(userId, username, role, departmentId));
        } catch (JwtException | IllegalArgumentException e) {
            return Optional.empty();
        }
    }

    private Optional<Long> asLong(Object value) {
        if (value == null) {
            return Optional.empty();
        }
        if (value instanceof Number number) {
            return Optional.of(number.longValue());
        }
        try {
            return Optional.of(Long.valueOf(String.valueOf(value)));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}
