package de.avatic.lcc.service.apps;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.configuration.apps.AppDTO;
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
import de.avatic.lcc.model.db.users.App;
import de.avatic.lcc.repositories.users.AppRepository;
import de.avatic.lcc.service.transformer.apps.AppTransformer;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Service
public class AppsService {


    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128;
    private static final int GCM_IV_LENGTH = 12;
    private final AppRepository appRepository;
    private final AppTransformer appTransformer;
    private final PasswordEncoder passwordEncoder;
    private final ObjectMapper objectMapper;
    private final Key signingKey;
    private final SecretKeySpec encryptionKey;


    public AppsService(@Value("${jwt.secret}") String secret, AppRepository appRepository, AppTransformer appTransformer, PasswordEncoder passwordEncoder, ObjectMapper objectMapper) {
        this.appRepository = appRepository;
        this.appTransformer = appTransformer;
        this.passwordEncoder = passwordEncoder;
        this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.objectMapper = objectMapper;

        // AES-256 Key aus JWT Secret ableiten
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        byte[] aesKey = new byte[32]; // 256 bit
        System.arraycopy(keyBytes, 0, aesKey, 0, Math.min(keyBytes.length, 32));
        this.encryptionKey = new SecretKeySpec(aesKey, "AES");
    }

    public List<AppDTO> listApps() {
        return appRepository.listApps().stream().map(appTransformer::toAppDTO).toList();
    }

    public AppDTO updateApp(AppDTO dto) {

        var newApp = dto.getId() == null;
        String appSecret = null;

        if (newApp) {
            dto.setClientId(generateAppId());
            appSecret = generateAppSecret();
            dto.setClientSecret(passwordEncoder.encode(appSecret));
        }

        var id = appRepository.update(appTransformer.toAppEntity(dto));

        if (newApp) {
            dto.setId(id);
            dto.setClientSecret(appSecret);
        }

        return dto;
    }

    private String generateAppId() {
        return UUID.randomUUID().toString();
    }

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

    public void deleteApp(Integer id) {
        appRepository.delete(id);
    }


    public Optional<App> validateApp(String clientId, String clientSecret) {
        var app = appRepository.getByClientId(clientId);

        if (app.isPresent() &&
                passwordEncoder.matches(clientSecret, app.get().getClientSecret())) {
            return app;
        }

        return Optional.empty();

    }

    public AppExchangeDTO exportApp(Integer id) {
        var app = appRepository.getById(id).map(appTransformer::toAppDTOWithHashedSecret);

        if (app.isEmpty()) {
            throw new IllegalArgumentException("App mit ID " + id + " nicht gefunden");
        }
        AppExchangeDTO exchangeDTO = new AppExchangeDTO();
        try {

            String json = objectMapper.writeValueAsString(app);
            byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);

            Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
            byte[] iv = new byte[GCM_IV_LENGTH];
            new SecureRandom().nextBytes(iv);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, gcmSpec);
            byte[] encryptedData = cipher.doFinal(jsonBytes);

            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
            mac.update(iv);
            mac.update(encryptedData);
            byte[] signature = mac.doFinal();

            byte[] bundle = new byte[iv.length + encryptedData.length + signature.length];
            System.arraycopy(iv, 0, bundle, 0, iv.length);
            System.arraycopy(encryptedData, 0, bundle, iv.length, encryptedData.length);
            System.arraycopy(signature, 0, bundle, iv.length + encryptedData.length, signature.length);


            exchangeDTO.setData(Base64.getEncoder().encodeToString(bundle));
        } catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
            throw new InternalErrorException("Fehler beim Exportieren der App");
        }


        return exchangeDTO;
    }

    public boolean importApp(AppExchangeDTO exchangeDTO)  {

        try {

            byte[] bundle = Base64.getDecoder().decode(exchangeDTO.getData());

            // 2. Bundle aufteilen
            if (bundle.length < GCM_IV_LENGTH + 32) {
                throw new IllegalArgumentException("Ungültiges Export-Bundle");
            }

            byte[] iv = new byte[GCM_IV_LENGTH];
            byte[] signature = new byte[32];
            byte[] encryptedData = new byte[bundle.length - GCM_IV_LENGTH - 32];

            System.arraycopy(bundle, 0, iv, 0, GCM_IV_LENGTH);
            System.arraycopy(bundle, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
            System.arraycopy(bundle, GCM_IV_LENGTH + encryptedData.length, signature, 0, 32);

            // 3. Signatur verifizieren
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
            mac.update(iv);
            mac.update(encryptedData);
            byte[] expectedSignature = mac.doFinal();

            if (!java.security.MessageDigest.isEqual(signature, expectedSignature)) {
                throw new SecurityException("Ungültige Signatur - Daten wurden manipuliert oder stammen nicht von dieser Instanz");
            }

            // 4. Entschlüsseln
            Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.DECRYPT_MODE, encryptionKey, gcmSpec);
            byte[] decryptedData = cipher.doFinal(encryptedData);

            // 5. JSON deserialisieren
            String json = new String(decryptedData, StandardCharsets.UTF_8);
            AppDTO dto = objectMapper.readValue(json, AppDTO.class);

            // 6. Prüfen ob App mit dieser Client-ID bereits existiert
            var existingApp = appRepository.getByClientId(dto.getClientId());
            if (existingApp.isPresent()) {
                throw new IllegalStateException(
                        "App mit Client-ID '" + dto.getClientId() + "' existiert bereits"
                );
            }

            App app = appTransformer.toAppEntityWithHashedSecret(dto);
            appRepository.update(app);
        } catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
           return false;
        }


        return true;
    }
}
