package org.mockserver.openapi;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.mockserver.log.model.LogEntry;
import org.mockserver.logging.MockServerLogger;
import org.mockserver.mock.Expectation;
import org.mockserver.model.AfterAction;
import org.mockserver.model.HttpResponse;
import org.mockserver.model.OpenAPIDefinition;
import org.mockserver.openapi.examples.ExampleBuilder;
import org.mockserver.openapi.examples.JsonNodeExampleSerializer;
import org.mockserver.openapi.examples.models.StringExample;
import org.mockserver.serialization.ObjectMapperFactory;

import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import static org.mockserver.model.OpenAPIDefinition.openAPI;
import static org.mockserver.openapi.OpenAPIParser.buildOpenAPI;
import static org.slf4j.event.Level.ERROR;
import static org.slf4j.event.Level.WARN;

public class OpenAPIConverter {

    private static final ObjectWriter OBJECT_WRITER = ObjectMapperFactory.createObjectMapper(new JsonNodeExampleSerializer()).writerWithDefaultPrettyPrinter();
    private static final int MAX_REF_DEPTH = 100;
    private static final int MAX_STRUCTURE_DEPTH = 1000;
    private final MockServerLogger mockServerLogger;

    public OpenAPIConverter(MockServerLogger mockServerLogger) {
        this.mockServerLogger = mockServerLogger;
    }

    public List<Expectation> buildExpectations(String specUrlOrPayload, Map<String, Object> operationsAndResponses) {
        return buildExpectations(specUrlOrPayload, operationsAndResponses, null);
    }

    public List<Expectation> buildExpectations(String specUrlOrPayload, Map<String, Object> operationsAndResponses, String contextPathPrefix) {
        OpenAPI openAPI = buildOpenAPI(specUrlOrPayload, mockServerLogger);
        String specKey = deriveSpecKey(openAPI, specUrlOrPayload);
        // Track how many times each operationId appears so we can disambiguate
        // when the same operationId maps to multiple expectations (e.g. multiple response codes)
        Map<String, Integer> operationIdCounts = new HashMap<>();
        return openAPI
            .getPaths()
            .values()
            .stream()
            .flatMap(pathItem ->
                pathItem
                    .readOperations()
                    .stream()
            )
            .filter(operation -> operationsAndResponses == null || operationsAndResponses.containsKey(operation.getOperationId()))
            .map(operation -> {
                String apiResponseKey = null;
                String exampleName = null;
                if (operationsAndResponses != null) {
                    Object value = operationsAndResponses.get(operation.getOperationId());
                    if (value instanceof String stringValue) {
                        apiResponseKey = stringValue;
                    } else if (value instanceof Map) {
                        @SuppressWarnings("unchecked")
                        Map<String, Object> richValue = (Map<String, Object>) value;
                        apiResponseKey = richValue.get("statusCode") != null ? String.valueOf(richValue.get("statusCode")) : null;
                        exampleName = richValue.get("exampleName") != null ? String.valueOf(richValue.get("exampleName")) : null;
                    }
                }
                OpenAPIDefinition openAPIDefinition = openAPI(specUrlOrPayload, operation.getOperationId());
                if (isNotBlank(contextPathPrefix)) {
                    openAPIDefinition.withContextPathPrefix(contextPathPrefix);
                }
                Expectation expectation = new Expectation(openAPIDefinition)
                    .thenRespond(buildHttpResponse(openAPI, operation.getResponses(), apiResponseKey, exampleName));
                List<AfterAction> afterActions = buildAfterActions(openAPI, operation);
                if (!afterActions.isEmpty()) {
                    expectation.withAfterActions(afterActions);
                }
                // Assign stable deterministic id: openapi:<specKey>:<operationId>[:<n>]
                String operationId = operation.getOperationId();
                int count = operationIdCounts.merge(operationId, 1, Integer::sum);
                String stableId = OpenApiSyncPlanner.OPENAPI_ID_PREFIX + specKey + ":" + operationId;
                if (count > 1) {
                    stableId += ":" + count;
                }
                expectation.withId(stableId);
                return expectation;
            })
            .collect(Collectors.toList());
    }

    /**
     * Derives a stable spec key from the parsed OpenAPI object. Uses the
     * spec title (sanitized) if available, otherwise falls back to a short
     * hash of the raw spec URL or payload.
     */
    static String deriveSpecKey(OpenAPI openAPI, String specUrlOrPayload) {
        String title = null;
        if (openAPI.getInfo() != null) {
            title = openAPI.getInfo().getTitle();
        }
        String key = OpenApiSyncPlanner.specKeyFromTitle(title);
        if (key == null) {
            key = OpenApiSyncPlanner.specKeyFromHash(specUrlOrPayload);
        }
        return key;
    }

    private List<AfterAction> buildAfterActions(OpenAPI openAPI, io.swagger.v3.oas.models.Operation operation) {
        List<AfterAction> afterActions = new ArrayList<>();
        Map<String, io.swagger.v3.oas.models.callbacks.Callback> callbacks = operation.getCallbacks();
        if (callbacks == null || callbacks.isEmpty()) {
            return afterActions;
        }
        for (Map.Entry<String, io.swagger.v3.oas.models.callbacks.Callback> callbackEntry : callbacks.entrySet()) {
            io.swagger.v3.oas.models.callbacks.Callback callback = callbackEntry.getValue();
            if (callback == null) {
                continue;
            }
            for (Map.Entry<String, io.swagger.v3.oas.models.PathItem> pathEntry : callback.entrySet()) {
                String callbackUrl = pathEntry.getKey();
                io.swagger.v3.oas.models.PathItem pathItem = pathEntry.getValue();
                if (pathItem == null) {
                    continue;
                }
                for (Map.Entry<io.swagger.v3.oas.models.PathItem.HttpMethod, io.swagger.v3.oas.models.Operation> opEntry : pathItem.readOperationsMap().entrySet()) {
                    io.swagger.v3.oas.models.PathItem.HttpMethod method = opEntry.getKey();
                    io.swagger.v3.oas.models.Operation callbackOp = opEntry.getValue();
                    try {
                        String resolvedUrl = resolveCallbackUrl(callbackUrl);
                        org.mockserver.model.HttpRequest callbackRequest = org.mockserver.model.HttpRequest.request()
                            .withMethod(method.name());
                        if (OpenApiRuntimeExpressionResolver.containsExpression(resolvedUrl)) {
                            // URL contains runtime expressions — store verbatim for fire-time resolution
                            callbackRequest.withPath(resolvedUrl);
                        } else if (resolvedUrl.startsWith("http://") || resolvedUrl.startsWith("https://")) {
                            URI uri = new URI(resolvedUrl);
                            callbackRequest
                                .withPath(uri.getPath() != null ? uri.getPath() : "/")
                                .withHeader("Host", uri.getHost() + (uri.getPort() > 0 ? ":" + uri.getPort() : ""))
                                .withSecure(resolvedUrl.startsWith("https://"));
                        } else {
                            callbackRequest.withPath(resolvedUrl);
                        }
                        if (callbackOp.getRequestBody() != null && callbackOp.getRequestBody().getContent() != null) {
                            callbackOp.getRequestBody().getContent().entrySet().stream().findFirst().ifPresent(contentEntry -> {
                                callbackRequest.withHeader("Content-Type", contentEntry.getKey());
                                MediaType mediaType = contentEntry.getValue();
                                if (mediaType != null && mediaType.getSchema() != null) {
                                    org.mockserver.openapi.examples.models.Example example = ExampleBuilder.fromSchema(
                                        mediaType.getSchema(),
                                        openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : null
                                    );
                                    if (example != null) {
                                        callbackRequest.withBody(serialise(example));
                                    }
                                }
                            });
                        }
                        afterActions.add(new AfterAction().withHttpRequest(callbackRequest));
                    } catch (Exception e) {
                        mockServerLogger.logEvent(
                            new LogEntry()
                                .setLogLevel(WARN)
                                .setMessageFormat("failed to build callback after-action for {} {} - {}")
                                .setArguments(method, callbackUrl, e.getMessage())
                        );
                    }
                }
            }
        }
        return afterActions;
    }

    /**
     * Preserves OpenAPI runtime expressions verbatim in callback URLs.
     * Expressions like {@code {$request.body#/callbackUrl}} are kept as-is and
     * resolved at callback fire-time by {@link OpenApiRuntimeExpressionResolver}.
     */
    private String resolveCallbackUrl(String callbackUrl) {
        return callbackUrl;
    }

    private HttpResponse buildHttpResponse(OpenAPI openAPI, ApiResponses apiResponses, String apiResponseKey) {
        return buildHttpResponse(openAPI, apiResponses, apiResponseKey, null);
    }

    private HttpResponse buildHttpResponse(OpenAPI openAPI, ApiResponses apiResponses, String apiResponseKey, String exampleName) {
        HttpResponse response = response();
        Optional
            .ofNullable(apiResponses)
            .flatMap(notNullApiResponses -> notNullApiResponses.entrySet().stream().filter(entry -> isBlank(apiResponseKey) | entry.getKey().equals(apiResponseKey)).findFirst())
            .ifPresent(apiResponse -> {
                if (!apiResponse.getKey().equalsIgnoreCase("default")) {
                    response.withStatusCode(Integer.parseInt(apiResponse.getKey()));
                }
                Optional
                    .ofNullable(apiResponse.getValue().getHeaders())
                    .map(Map::entrySet)
                    .map(Set::stream)
                    .ifPresent(stream -> stream
                        .forEach(entry -> {
                            Header value = entry.getValue();
                            Object headerExample = findHeaderExample(value, openAPI, exampleName);
                            if (headerExample != null) {
                                response.withHeader(entry.getKey(), String.valueOf(headerExample));
                            } else if (value.getSchema() != null) {
                                org.mockserver.openapi.examples.models.Example generatedExample = ExampleBuilder.fromSchema(value.getSchema(), openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : null);
                                if (generatedExample instanceof StringExample stringExample) {
                                    response.withHeader(entry.getKey(), stringExample.getValue());
                                } else {
                                    response.withHeader(entry.getKey(), serialise(generatedExample));
                                }
                            }
                        })
                    );
                Optional
                    .ofNullable(apiResponse.getValue().getContent())
                    .flatMap(content -> content
                        .entrySet()
                        .stream()
                        .findFirst()
                    )
                    .ifPresent(contentType -> {
                        response.withHeader("content-type", contentType.getKey());
                        Optional
                            .ofNullable(contentType.getValue())
                            .ifPresent(mediaType -> {
                                Object example = findExample(mediaType, openAPI, exampleName);
                                if (example != null) {
                                    if (isJsonContentType(contentType.getKey())) {
                                        response.withBody(json(serialise(example)));
                                    } else {
                                        response.withBody(String.valueOf(example));
                                    }
                                } else if (mediaType.getSchema() != null) {
                                    Object schemaExample = resolveSchemaExample(mediaType.getSchema(), openAPI);
                                    if (schemaExample != null) {
                                        if (isJsonContentType(contentType.getKey())) {
                                            response.withBody(json(serialise(schemaExample)));
                                        } else {
                                            response.withBody(serialise(schemaExample));
                                        }
                                    } else {
                                        org.mockserver.openapi.examples.models.Example generatedExample = ExampleBuilder.fromSchema(mediaType.getSchema(), openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : null);
                                        if (generatedExample instanceof StringExample stringExample) {
                                            if (isJsonContentType(contentType.getKey())) {
                                                response.withBody(json(serialise(stringExample.getValue())));
                                            } else {
                                                response.withBody(stringExample.getValue());
                                            }
                                        } else if (generatedExample != null) {
                                            String serialise = serialise(generatedExample);
                                            if (isJsonContentType(contentType.getKey())) {
                                                response.withBody(json(serialise));
                                            } else {
                                                response.withBody(serialise);
                                            }
                                        }
                                    }
                                }
                            });
                    });
            });
        return response;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Object resolveSchemaExample(io.swagger.v3.oas.models.media.Schema schema, OpenAPI openAPI) {
        return resolveSchemaExample(schema, openAPI, Collections.newSetFromMap(new IdentityHashMap<>()));
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Object resolveSchemaExample(io.swagger.v3.oas.models.media.Schema schema, OpenAPI openAPI, Set<io.swagger.v3.oas.models.media.Schema> activeStack) {
        if (schema == null || !activeStack.add(schema)) {
            return null;
        }
        try {
            if (schema.getExample() != null) {
                return resolveExampleRefs(schema.getExample(), openAPI);
            }
            if (schema instanceof ComposedSchema composedSchema) {
                if (composedSchema.getAllOf() != null) {
                    Map<String, Object> merged = new LinkedHashMap<>();
                    for (Schema<?> subSchema : composedSchema.getAllOf()) {
                        Object subExample = resolveSchemaExample(subSchema, openAPI, activeStack);
                        if (subExample instanceof Map) {
                            merged.putAll((Map<String, Object>) subExample);
                        }
                    }
                    if (composedSchema.getProperties() != null) {
                        Map<String, Schema> ownProperties = composedSchema.getProperties();
                        for (Map.Entry<String, Schema> entry : ownProperties.entrySet()) {
                            Object propExample = resolveSchemaExample(entry.getValue(), openAPI, activeStack);
                            if (propExample == null) {
                                return null;
                            }
                            merged.put(entry.getKey(), propExample);
                        }
                    }
                    return merged.isEmpty() ? null : merged;
                }
                if (composedSchema.getAnyOf() != null) {
                    for (Schema<?> subSchema : composedSchema.getAnyOf()) {
                        Object subExample = resolveSchemaExample(subSchema, openAPI, activeStack);
                        if (subExample != null) {
                            return subExample;
                        }
                    }
                }
                if (composedSchema.getOneOf() != null) {
                    for (Schema<?> subSchema : composedSchema.getOneOf()) {
                        Object subExample = resolveSchemaExample(subSchema, openAPI, activeStack);
                        if (subExample != null) {
                            return subExample;
                        }
                    }
                }
            }
            if (schema.getProperties() != null) {
                Map<String, io.swagger.v3.oas.models.media.Schema> properties = schema.getProperties();
                Map<String, Object> result = new LinkedHashMap<>();
                for (Map.Entry<String, io.swagger.v3.oas.models.media.Schema> entry : properties.entrySet()) {
                    Object propExample = resolveSchemaExample(entry.getValue(), openAPI, activeStack);
                    if (propExample == null) {
                        return null;
                    }
                    result.put(entry.getKey(), propExample);
                }
                return result.isEmpty() ? null : result;
            }
            if (schema instanceof io.swagger.v3.oas.models.media.ArraySchema arraySchema) {
                if (arraySchema.getItems() != null) {
                    Object itemExample = resolveSchemaExample(arraySchema.getItems(), openAPI, activeStack);
                    if (itemExample != null) {
                        return Collections.singletonList(itemExample);
                    }
                }
            }
            return null;
        } finally {
            activeStack.remove(schema);
        }
    }

    public static boolean isJsonContentType(String contentType) {
        return org.mockserver.model.MediaType.parse(contentType).isJson();
    }

    private Object findHeaderExample(Header value, OpenAPI openAPI, String exampleName) {
        if (exampleName != null && value.getExamples() != null && value.getExamples().containsKey(exampleName)) {
            Example example = value.getExamples().get(exampleName);
            if (example != null) {
                Object resolved = resolveExampleRefs(example.getValue(), openAPI);
                return resolved != null ? resolved : example.getValue();
            }
        }
        if (value.getExample() instanceof Example exampleObj) {
            Object resolved = resolveExampleRefs(exampleObj.getValue(), openAPI);
            return resolved != null ? resolved : exampleObj.getValue();
        } else if (value.getExample() != null) {
            return resolveExampleRefs(value.getExample(), openAPI);
        } else if (value.getExamples() != null && !value.getExamples().isEmpty()) {
            Example example = value.getExamples().values().stream().findFirst().orElse(null);
            if (example != null) {
                Object resolved = resolveExampleRefs(example.getValue(), openAPI);
                return resolved != null ? resolved : example.getValue();
            }
        }
        return null;
    }

    private Object findExample(MediaType mediaType, OpenAPI openAPI, String exampleName) {
        Object example = null;
        if (exampleName != null && mediaType.getExamples() != null && mediaType.getExamples().containsKey(exampleName)) {
            Example namedExample = mediaType.getExamples().get(exampleName);
            if (namedExample != null) {
                example = namedExample.getValue();
            }
        } else if (mediaType.getExample() != null) {
            Object raw = mediaType.getExample();
            if (raw instanceof Example rawExample) {
                example = rawExample.getValue();
            } else {
                example = raw;
            }
        } else if (mediaType.getExamples() != null && !mediaType.getExamples().isEmpty()) {
            Example namedExample = mediaType.getExamples().values().stream().findFirst().orElse(null);
            if (namedExample != null) {
                example = namedExample.getValue();
            }
        }
        if (example != null) {
            example = resolveExampleRefs(example, openAPI);
        }
        return example;
    }

    @SuppressWarnings("unchecked")
    private Object resolveExampleRefs(Object value, OpenAPI openAPI) {
        return resolveExampleRefs(value, openAPI, new HashSet<>(), 0, 0);
    }

    @SuppressWarnings("unchecked")
    private Object resolveExampleRefs(Object value, OpenAPI openAPI, Set<String> activeRefChain, int refDepth, int structureDepth) {
        if (structureDepth > MAX_STRUCTURE_DEPTH) {
            return value;
        }
        if (value instanceof ObjectNode node) {
            if (node.size() == 1 && node.has("$ref")) {
                String ref = node.get("$ref").asText();
                if (activeRefChain.contains(ref)) {
                    mockServerLogger.logEvent(
                        new LogEntry()
                            .setLogLevel(WARN)
                            .setMessageFormat("cyclic $ref detected for {} — returning literal value")
                            .setArguments(ref)
                    );
                    return value;
                }
                if (refDepth >= MAX_REF_DEPTH) {
                    mockServerLogger.logEvent(
                        new LogEntry()
                            .setLogLevel(WARN)
                            .setMessageFormat("$ref resolution exceeded maximum depth of {} for {} — returning literal value")
                            .setArguments(MAX_REF_DEPTH, ref)
                    );
                    return value;
                }
                Object resolved = resolveRef(ref, openAPI);
                if (resolved != null) {
                    activeRefChain.add(ref);
                    Object result = resolveExampleRefs(resolved, openAPI, activeRefChain, refDepth + 1, structureDepth + 1);
                    activeRefChain.remove(ref);
                    return result;
                }
            }
            ObjectNode resolvedNode = node.objectNode();
            node.properties().forEach(entry -> {
                Object resolvedField = resolveExampleRefs(entry.getValue(), openAPI, activeRefChain, refDepth, structureDepth + 1);
                if (resolvedField instanceof JsonNode) {
                    resolvedNode.set(entry.getKey(), (JsonNode) resolvedField);
                } else {
                    resolvedNode.putPOJO(entry.getKey(), resolvedField);
                }
            });
            return resolvedNode;
        } else if (value instanceof ArrayNode node) {
            ArrayNode resolvedNode = node.arrayNode();
            for (JsonNode item : node) {
                Object resolvedItem = resolveExampleRefs(item, openAPI, activeRefChain, refDepth, structureDepth + 1);
                if (resolvedItem instanceof JsonNode) {
                    resolvedNode.add((JsonNode) resolvedItem);
                } else {
                    resolvedNode.addPOJO(resolvedItem);
                }
            }
            return resolvedNode;
        } else if (value instanceof Map) {
            Map<String, Object> map = (Map<String, Object>) value;
            if (map.size() == 1 && map.containsKey("$ref")) {
                String ref = String.valueOf(map.get("$ref"));
                if (activeRefChain.contains(ref)) {
                    mockServerLogger.logEvent(
                        new LogEntry()
                            .setLogLevel(WARN)
                            .setMessageFormat("cyclic $ref detected for {} — returning literal value")
                            .setArguments(ref)
                    );
                    return value;
                }
                if (refDepth >= MAX_REF_DEPTH) {
                    mockServerLogger.logEvent(
                        new LogEntry()
                            .setLogLevel(WARN)
                            .setMessageFormat("$ref resolution exceeded maximum depth of {} for {} — returning literal value")
                            .setArguments(MAX_REF_DEPTH, ref)
                    );
                    return value;
                }
                Object resolved = resolveRef(ref, openAPI);
                if (resolved != null) {
                    activeRefChain.add(ref);
                    Object result = resolveExampleRefs(resolved, openAPI, activeRefChain, refDepth + 1, structureDepth + 1);
                    activeRefChain.remove(ref);
                    return result;
                }
            }
            Map<String, Object> resolvedMap = new LinkedHashMap<>();
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                resolvedMap.put(entry.getKey(), resolveExampleRefs(entry.getValue(), openAPI, activeRefChain, refDepth, structureDepth + 1));
            }
            return resolvedMap;
        } else if (value instanceof List) {
            List<Object> list = (List<Object>) value;
            List<Object> resolvedList = new ArrayList<>(list.size());
            for (Object item : list) {
                resolvedList.add(resolveExampleRefs(item, openAPI, activeRefChain, refDepth, structureDepth + 1));
            }
            return resolvedList;
        }
        return value;
    }

    private Object resolveRef(String ref, OpenAPI openAPI) {
        if (ref != null && ref.startsWith("#/components/examples/") && openAPI.getComponents() != null && openAPI.getComponents().getExamples() != null) {
            String path = ref.substring("#/components/examples/".length());
            String[] parts = path.split("/");
            if (parts.length >= 1) {
                Example componentExample = openAPI.getComponents().getExamples().get(parts[0]);
                if (componentExample != null) {
                    return componentExample.getValue();
                }
            }
            mockServerLogger.logEvent(
                new LogEntry()
                    .setLogLevel(WARN)
                    .setMessageFormat("unable to resolve $ref {} in example")
                    .setArguments(ref)
            );
        }
        return null;
    }

    private String serialise(Object example) {
        try {
            return OBJECT_WRITER.writeValueAsString(example);
        } catch (Throwable throwable) {
            mockServerLogger.logEvent(
                new LogEntry()
                    .setLogLevel(ERROR)
                    .setMessageFormat("exception while serialising " + example.getClass() + " {}")
                    .setArguments(example)
                    .setThrowable(throwable)
            );
            return "";
        }
    }
}
