package dev.lions.unionflow.server.service;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.Normalizer;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;

/**
 * Service de migration one-shot : crée les Keycloak 26 Organizations correspondant
 * à chaque Organisation UnionFlow, assigne les rôles standards et migre les memberships.
 *
 * <p>Idempotent : si {@code keycloak_org_id} est déjà renseigné pour une org,
 * elle est ignorée (pas de doublon).
 *
 * <p>Déclenchement : endpoint admin {@code POST /api/admin/keycloak/migrer-organisations}.
 */
@Slf4j
@ApplicationScoped
public class MigrerOrganisationsVersKeycloakService {

    /** Rôles Organization standards créés dans chaque Keycloak Organization. */
    private static final List<String> ROLES_STANDARDS = List.of(
            "ADMIN_ORGANISATION", "TRESORIER", "SECRETAIRE",
            "COMMISSAIRE_COMPTES", "MEMBRE_ACTIF"
    );

    @ConfigProperty(name = "keycloak.admin.url", defaultValue = "http://localhost:8180")
    String keycloakUrl;

    @ConfigProperty(name = "keycloak.admin.username", defaultValue = "admin")
    String adminUsername;

    @ConfigProperty(name = "keycloak.admin.password", defaultValue = "admin")
    String adminPassword;

    @ConfigProperty(name = "keycloak.admin.realm", defaultValue = "unionflow")
    String realm;

    @Inject
    OrganisationRepository organisationRepository;

    @Inject
    MembreOrganisationRepository membreOrganisationRepository;

    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    private final ObjectMapper mapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    /**
     * Point d'entrée principal — migre toutes les organisations sans {@code keycloak_org_id}.
     *
     * @return rapport de migration
     */
    @Transactional
    public MigrationReport migrerToutesLesOrganisations() throws Exception {
        String token = getAdminToken();
        List<Organisation> orgs = organisationRepository.listAll();

        int crees = 0, ignores = 0, erreurs = 0;

        for (Organisation org : orgs) {
            if (org.getKeycloakOrgId() != null) {
                ignores++;
                log.debug("Org '{}' déjà migrée (kcOrgId={}), ignorée.", org.getNom(), org.getKeycloakOrgId());
                continue;
            }

            try {
                UUID kcOrgId = creerOrganisationKeycloak(token, org);
                creerRolesOrganisation(token, kcOrgId);
                migrerMemberships(token, kcOrgId, org);

                org.setKeycloakOrgId(kcOrgId);
                organisationRepository.persist(org);
                crees++;
                log.info("Organisation '{}' migrée → keycloak_org_id={}", org.getNom(), kcOrgId);

            } catch (Exception e) {
                erreurs++;
                log.error("Échec migration org '{}' (id={}): {}", org.getNom(), org.getId(), e.getMessage(), e);
            }
        }

        return new MigrationReport(orgs.size(), crees, ignores, erreurs);
    }

    // ── Création Organization Keycloak ──────────────────────────────────────────

    private UUID creerOrganisationKeycloak(String token, Organisation org) throws Exception {
        ObjectNode body = mapper.createObjectNode();
        body.put("name", org.getNom());
        body.put("alias", slugify(org.getNom()));
        body.put("enabled", true);

        ObjectNode attrs = mapper.createObjectNode();
        attrs.putArray("unionflow_id").add(org.getId().toString());
        attrs.putArray("type_organisation").add(org.getTypeOrganisation() != null ? org.getTypeOrganisation() : "");
        if (org.getCategorieType() != null) {
            attrs.putArray("categorie").add(org.getCategorieType());
        }
        body.set("attributes", attrs);

        // Ajouter le domaine email si disponible
        if (org.getEmail() != null) {
            String domaine = org.getEmail().contains("@") ? org.getEmail().split("@")[1] : "";
            if (!domaine.isBlank()) {
                ArrayNode domains = mapper.createArrayNode();
                ObjectNode domainObj = mapper.createObjectNode();
                domainObj.put("name", domaine);
                domainObj.put("verified", false);
                domains.add(domainObj);
                body.set("domains", domains);
            }
        }

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/organizations"))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)))
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        // 201 Created → Location header contient l'URL avec l'ID
        if (response.statusCode() == 201) {
            String location = response.headers().firstValue("Location").orElseThrow(
                    () -> new RuntimeException("Keycloak 201 mais sans header Location pour org: " + org.getNom()));
            String kcOrgId = location.substring(location.lastIndexOf('/') + 1);
            return UUID.fromString(kcOrgId);
        }

        // 409 = alias déjà pris → chercher l'org existante par alias
        if (response.statusCode() == 409) {
            log.warn("Organisation '{}' déjà présente dans Keycloak (409), recherche par alias.", org.getNom());
            return chercherOrganisationParAlias(token, slugify(org.getNom()));
        }

        throw new RuntimeException("Échec création Keycloak Org '" + org.getNom()
                + "' (HTTP " + response.statusCode() + "): " + response.body());
    }

    private UUID chercherOrganisationParAlias(String token, String alias) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/organizations?search=" + alias + "&max=10"))
                .header("Authorization", "Bearer " + token)
                .GET()
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException("Impossible de rechercher l'org par alias '" + alias + "'");
        }

        var results = mapper.readTree(response.body());
        for (var node : results) {
            if (alias.equals(node.path("alias").asText())) {
                return UUID.fromString(node.path("id").asText());
            }
        }
        throw new RuntimeException("Organisation avec alias '" + alias + "' introuvable dans Keycloak.");
    }

    // ── Création rôles standards ────────────────────────────────────────────────

    private void creerRolesOrganisation(String token, UUID kcOrgId) throws Exception {
        for (String roleName : ROLES_STANDARDS) {
            ObjectNode roleBody = mapper.createObjectNode();
            roleBody.put("name", roleName);

            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(keycloakUrl + "/admin/realms/" + realm
                            + "/organizations/" + kcOrgId + "/roles"))
                    .header("Authorization", "Bearer " + token)
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(roleBody)))
                    .timeout(Duration.ofSeconds(10))
                    .build();

            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 201 && response.statusCode() != 409) {
                log.warn("Impossible de créer le rôle '{}' pour kcOrgId={} (HTTP {}): {}",
                        roleName, kcOrgId, response.statusCode(), response.body());
            }
        }
    }

    // ── Migration memberships ───────────────────────────────────────────────────

    private void migrerMemberships(String token, UUID kcOrgId, Organisation org) {
        List<MembreOrganisation> memberships = membreOrganisationRepository
                .find("organisation.id = ?1 AND actif = true", org.getId())
                .list();

        for (MembreOrganisation mo : memberships) {
            if (mo.getMembre() == null || mo.getMembre().getKeycloakId() == null) {
                continue;
            }

            String kcUserId = mo.getMembre().getKeycloakId().toString();
            try {
                ajouterMembreKeycloakOrg(token, kcOrgId, kcUserId);

                if (mo.getRoleOrg() != null && ROLES_STANDARDS.contains(mo.getRoleOrg())) {
                    assignerRoleOrganisation(token, kcOrgId, kcUserId, mo.getRoleOrg());
                }
            } catch (Exception e) {
                log.warn("Impossible de migrer le membership keycloakId={} → kcOrg={}: {}",
                        kcUserId, kcOrgId, e.getMessage());
            }
        }
    }

    private void ajouterMembreKeycloakOrg(String token, UUID kcOrgId, String kcUserId) throws Exception {
        ObjectNode body = mapper.createObjectNode();
        body.put("id", kcUserId);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/admin/realms/" + realm
                        + "/organizations/" + kcOrgId + "/members"))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)))
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 201 && response.statusCode() != 409) {
            throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
        }
    }

    private void assignerRoleOrganisation(String token, UUID kcOrgId, String kcUserId,
                                          String roleName) throws Exception {
        // 1. Récupérer l'ID du rôle
        HttpRequest getRoles = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/admin/realms/" + realm
                        + "/organizations/" + kcOrgId + "/roles"))
                .header("Authorization", "Bearer " + token)
                .GET()
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> rolesResponse = httpClient.send(getRoles, HttpResponse.BodyHandlers.ofString());
        if (rolesResponse.statusCode() != 200) return;

        var roles = mapper.readTree(rolesResponse.body());
        String roleId = null;
        for (var role : roles) {
            if (roleName.equals(role.path("name").asText())) {
                roleId = role.path("id").asText();
                break;
            }
        }
        if (roleId == null) return;

        // 2. Assigner le rôle au membre
        ArrayNode assignBody = mapper.createArrayNode();
        ObjectNode roleRef = mapper.createObjectNode();
        roleRef.put("id", roleId);
        roleRef.put("name", roleName);
        assignBody.add(roleRef);

        HttpRequest assignRequest = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/admin/realms/" + realm
                        + "/organizations/" + kcOrgId + "/members/" + kcUserId + "/roles"))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(assignBody)))
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> assignResponse = httpClient.send(assignRequest, HttpResponse.BodyHandlers.ofString());
        if (assignResponse.statusCode() != 201 && assignResponse.statusCode() != 204) {
            log.warn("Impossible d'assigner le rôle '{}' à l'utilisateur {} (HTTP {})",
                    roleName, kcUserId, assignResponse.statusCode());
        }
    }

    // ── Auth admin ──────────────────────────────────────────────────────────────

    private String getAdminToken() throws Exception {
        String body = "client_id=admin-cli"
                + "&username=" + java.net.URLEncoder.encode(adminUsername, java.nio.charset.StandardCharsets.UTF_8)
                + "&password=" + java.net.URLEncoder.encode(adminPassword, java.nio.charset.StandardCharsets.UTF_8)
                + "&grant_type=password";

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(keycloakUrl + "/realms/master/protocol/openid-connect/token"))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .timeout(Duration.ofSeconds(10))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException("Échec auth admin Keycloak (HTTP " + response.statusCode() + ")");
        }

        return mapper.readTree(response.body()).get("access_token").asText();
    }

    // ── Utilitaires ─────────────────────────────────────────────────────────────

    private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^a-z0-9-]");
    private static final Pattern MULTIPLE_DASHES = Pattern.compile("-{2,}");

    static String slugify(String input) {
        if (input == null) return "org-" + UUID.randomUUID().toString().substring(0, 8);
        String normalized = Normalizer.normalize(input.toLowerCase(), Normalizer.Form.NFD)
                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
        String slug = NON_ALPHANUMERIC.matcher(normalized.replace(' ', '-')).replaceAll("");
        slug = MULTIPLE_DASHES.matcher(slug).replaceAll("-").replaceAll("^-|-$", "");
        return slug.isBlank() ? "org-" + UUID.randomUUID().toString().substring(0, 8) : slug;
    }

    // ── Rapport ─────────────────────────────────────────────────────────────────

    public record MigrationReport(int total, int crees, int ignores, int erreurs) {
        public boolean success() {
            return erreurs == 0;
        }
    }
}
