package net.siegeln.cameleer.saas.identity;

import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
public class LogtoManagementClient {

    private static final Logger log = LoggerFactory.getLogger(LogtoManagementClient.class);

    private final LogtoConfig config;
    private final RestClient restClient;

    private String cachedToken;
    private Instant tokenExpiry = Instant.MIN;

    public LogtoManagementClient(LogtoConfig config) {
        this.config = config;
        this.restClient = RestClient.builder()
                .defaultHeader("Content-Type", "application/json")
                .build();
    }

    public boolean isAvailable() {
        return config.isConfigured();
    }

    public String createOrganization(String name, String description) {
        if (!isAvailable()) {
            log.warn("Logto not configured — skipping organization creation for '{}'", name);
            return null;
        }

        var body = Map.of("name", name, "description", description != null ? description : "");

        var response = restClient.post()
                .uri(config.getLogtoEndpoint() + "/api/organizations")
                .header("Authorization", "Bearer " + getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .body(body)
                .retrieve()
                .body(JsonNode.class);

        return response != null ? response.get("id").asText() : null;
    }

    public void addUserToOrganization(String orgId, String userId) {
        if (!isAvailable()) return;

        restClient.post()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users")
                .header("Authorization", "Bearer " + getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("userIds", new String[]{userId}))
                .retrieve()
                .toBodilessEntity();
    }

    public void deleteOrganization(String orgId) {
        if (!isAvailable()) return;

        restClient.delete()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId)
                .header("Authorization", "Bearer " + getAccessToken())
                .retrieve()
                .toBodilessEntity();
    }

    /** Add redirect URIs to a Logto application (for OIDC callback registration). */
    @SuppressWarnings("unchecked")
    public void addAppRedirectUris(String appId, List<String> redirectUris, List<String> postLogoutUris) {
        if (!isAvailable() || appId == null) return;
        try {
            String token = getAccessToken();
            // GET current app config
            var app = (Map<String, Object>) restClient.get()
                .uri(config.getLogtoEndpoint() + "/api/applications/" + appId)
                .header("Authorization", "Bearer " + token)
                .retrieve()
                .body(Map.class);
            if (app == null) return;

            var metadata = (Map<String, Object>) app.get("oidcClientMetadata");
            if (metadata == null) return;

            // Merge new URIs with existing
            var existingRedirects = new ArrayList<>((List<String>) metadata.getOrDefault("redirectUris", List.of()));
            var existingPostLogout = new ArrayList<>((List<String>) metadata.getOrDefault("postLogoutRedirectUris", List.of()));
            for (String uri : redirectUris) {
                if (!existingRedirects.contains(uri)) existingRedirects.add(uri);
            }
            for (String uri : postLogoutUris) {
                if (!existingPostLogout.contains(uri)) existingPostLogout.add(uri);
            }

            // PATCH app with updated URIs
            restClient.patch()
                .uri(config.getLogtoEndpoint() + "/api/applications/" + appId)
                .header("Authorization", "Bearer " + token)
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("oidcClientMetadata", Map.of(
                    "redirectUris", existingRedirects,
                    "postLogoutRedirectUris", existingPostLogout
                )))
                .retrieve()
                .toBodilessEntity();
            log.info("Updated redirect URIs for app {}: added {}", appId, redirectUris);
        } catch (Exception e) {
            log.warn("Failed to update redirect URIs for app {}: {}", appId, e.getMessage());
        }
    }

    public List<Map<String, String>> getUserOrganizations(String userId) {
        if (!isAvailable()) return List.of();

        try {
            var response = restClient.get()
                    .uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/organizations")
                    .header("Authorization", "Bearer " + getAccessToken())
                    .retrieve()
                    .body(JsonNode.class);

            List<Map<String, String>> orgs = new ArrayList<>();
            if (response != null && response.isArray()) {
                for (var node : response) {
                    orgs.add(Map.of(
                            "id", node.get("id").asText(),
                            "name", node.get("name").asText()
                    ));
                }
            }
            return orgs;
        } catch (Exception e) {
            log.warn("Failed to get user organizations for {}: {}", userId, e.getMessage());
            return List.of();
        }
    }

    /** List members of a Logto organization. */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> listOrganizationMembers(String orgId) {
        if (!isAvailable()) return List.of();
        try {
            var resp = restClient.get()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users")
                .header("Authorization", "Bearer " + getAccessToken())
                .retrieve()
                .body(List.class);
            return resp != null ? resp : List.of();
        } catch (Exception e) {
            log.warn("Failed to list org members for {}: {}", orgId, e.getMessage());
            return List.of();
        }
    }

    /** Get roles assigned to a user within an organization. */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> getUserOrganizationRoles(String orgId, String userId) {
        if (!isAvailable()) return List.of();
        try {
            var resp = restClient.get()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId + "/roles")
                .header("Authorization", "Bearer " + getAccessToken())
                .retrieve()
                .body(List.class);
            return resp != null ? resp : List.of();
        } catch (Exception e) {
            log.warn("Failed to get user roles: {}", e.getMessage());
            return List.of();
        }
    }

    /** Assign a role to a user in an organization. */
    public void assignOrganizationRole(String orgId, String userId, String roleId) {
        if (!isAvailable()) return;
        try {
            restClient.post()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId + "/roles")
                .header("Authorization", "Bearer " + getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("organizationRoleIds", List.of(roleId)))
                .retrieve()
                .toBodilessEntity();
        } catch (Exception e) {
            log.warn("Failed to assign role: {}", e.getMessage());
        }
    }

    /** Remove a user from an organization. */
    public void removeUserFromOrganization(String orgId, String userId) {
        if (!isAvailable()) return;
        try {
            restClient.delete()
                .uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId)
                .header("Authorization", "Bearer " + getAccessToken())
                .retrieve()
                .toBodilessEntity();
        } catch (Exception e) {
            log.warn("Failed to remove user from org: {}", e.getMessage());
        }
    }

    /** Create a user in Logto and add to organization with role. */
    @SuppressWarnings("unchecked")
    public String createAndInviteUser(String email, String orgId, String roleId) {
        if (!isAvailable()) return null;
        try {
            var userResp = (Map<String, Object>) restClient.post()
                .uri(config.getLogtoEndpoint() + "/api/users")
                .header("Authorization", "Bearer " + getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("primaryEmail", email, "name", email.split("@")[0]))
                .retrieve()
                .body(Map.class);
            String userId = String.valueOf(userResp.get("id"));
            addUserToOrganization(orgId, userId);
            if (roleId != null) {
                assignOrganizationRole(orgId, userId, roleId);
            }
            return userId;
        } catch (Exception e) {
            log.error("Failed to create and invite user: {}", e.getMessage());
            throw new RuntimeException("Invite failed: " + e.getMessage(), e);
        }
    }

    /** Create a user with username/password and add to org with role. */
    @SuppressWarnings("unchecked")
    public String createUserWithPassword(String username, String password, String orgId, String roleId) {
        if (!isAvailable()) return null;
        try {
            var userResp = (Map<String, Object>) restClient.post()
                .uri(config.getLogtoEndpoint() + "/api/users")
                .header("Authorization", "Bearer " + getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("username", username, "password", password, "name", username))
                .retrieve()
                .body(Map.class);
            String userId = String.valueOf(userResp.get("id"));
            addUserToOrganization(orgId, userId);
            if (roleId != null) {
                assignOrganizationRole(orgId, userId, roleId);
            }
            log.info("Created user '{}' and added to org {} with role {}", username, orgId, roleId);
            return userId;
        } catch (Exception e) {
            log.error("Failed to create user '{}': {}", username, e.getMessage());
            throw new RuntimeException("User creation failed: " + e.getMessage(), e);
        }
    }

    /** Find org role ID by name (e.g., "owner", "operator", "viewer"). */
    @SuppressWarnings("unchecked")
    public String findOrgRoleIdByName(String roleName) {
        var roles = listOrganizationRoles();
        return roles.stream()
            .filter(r -> roleName.equals(r.get("name")))
            .map(r -> String.valueOf(r.get("id")))
            .findFirst()
            .orElse(null);
    }

    /** List available organization roles. */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> listOrganizationRoles() {
        if (!isAvailable()) return List.of();
        try {
            var resp = restClient.get()
                .uri(config.getLogtoEndpoint() + "/api/organization-roles")
                .header("Authorization", "Bearer " + getAccessToken())
                .retrieve()
                .body(List.class);
            return resp != null ? resp : List.of();
        } catch (Exception e) {
            log.warn("Failed to list org roles: {}", e.getMessage());
            return List.of();
        }
    }

    private static final String MGMT_API_RESOURCE = "https://default.logto.app/api";

    private synchronized String getAccessToken() {
        if (cachedToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(60))) {
            return cachedToken;
        }

        try {
            var response = restClient.post()
                    .uri(config.getLogtoEndpoint() + "/oidc/token")
                    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                    .body("grant_type=client_credentials"
                            + "&client_id=" + config.getM2mClientId()
                            + "&client_secret=" + config.getM2mClientSecret()
                            + "&resource=" + MGMT_API_RESOURCE
                            + "&scope=all")
                    .retrieve()
                    .body(JsonNode.class);

            cachedToken = response.get("access_token").asText();
            long expiresIn = response.get("expires_in").asLong();
            tokenExpiry = Instant.now().plusSeconds(expiresIn);

            return cachedToken;
        } catch (Exception e) {
            log.error("Failed to get Logto Management API token", e);
            throw new RuntimeException("Logto authentication failed", e);
        }
    }
}
