package org.openstack4j.openstack.internal;

import java.util.*;

import java.util.function.Function;

import org.openstack4j.api.client.CloudProvider;
import org.openstack4j.api.types.ServiceType;
import org.openstack4j.core.transport.*;
import org.openstack4j.core.transport.HttpRequest.RequestBuilder;
import org.openstack4j.core.transport.internal.HttpExecutor;
import org.openstack4j.model.ModelEntity;
import org.openstack4j.model.common.ActionResponse;
import org.openstack4j.model.common.Payload;
import org.openstack4j.model.identity.AuthVersion;
import org.openstack4j.model.identity.v2.Access;
import org.openstack4j.model.identity.v3.Service;

import static org.openstack4j.core.transport.ClientConstants.HEADER_USER_AGENT;
import static org.openstack4j.core.transport.ClientConstants.USER_AGENT;

public class BaseOpenStackService {

    private static ThreadLocal<String> reqIdContainer = new ThreadLocal<>();
    private ServiceType serviceType = ServiceType.IDENTITY;
    private Function<String, String> endpointFunc;

    protected BaseOpenStackService() {
    }

    protected BaseOpenStackService(ServiceType serviceType) {
        this(serviceType, null);
    }

    protected BaseOpenStackService(ServiceType serviceType, Function<String, String> endpointFunc) {
        this.serviceType = serviceType;
        this.endpointFunc = endpointFunc;
    }

    public String getXOpenstackRequestId() {
        return reqIdContainer.get();
    }

    protected <R> Invocation<R> get(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.GET);
    }

    protected Invocation<ActionResponse> getWithResponse(String... path) {
        return builder(ActionResponse.class, path, HttpMethod.GET);
    }

    protected <R> Invocation<R> post(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.POST);
    }

    protected Invocation<ActionResponse> postWithResponse(String... path) {
        return builder(ActionResponse.class, path, HttpMethod.POST);
    }

    protected <R> Invocation<R> put(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.PUT);
    }

    protected Invocation<ActionResponse> putWithResponse(String... path) {
        return builder(ActionResponse.class, path, HttpMethod.PUT);
    }

    protected <R> Invocation<R> patch(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.PATCH);
    }

    protected Invocation<ActionResponse> patchWithResponse(String... path) {
        return builder(ActionResponse.class, path, HttpMethod.PATCH);
    }

    protected <R> Invocation<R> delete(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.DELETE);
    }

    protected Invocation<ActionResponse> deleteWithResponse(String... path) {
        return builder(ActionResponse.class, path, HttpMethod.DELETE);
    }

    protected <R> Invocation<R> head(Class<R> returnType, String... path) {
        return builder(returnType, path, HttpMethod.HEAD);
    }

    protected <R> Invocation<R> request(HttpMethod method, Class<R> returnType, String path) {
        return builder(returnType, path, method);
    }

    protected String uri(String path, Object... params) {
        if (params.length == 0)
            return path;
        return String.format(path, params);
    }

    private <R> Invocation<R> builder(Class<R> returnType, String[] path, HttpMethod method) {
        return builder(returnType, String.join("", path), method);
    }

    private <R> Invocation<R> builder(Class<R> returnType, String path, HttpMethod method) {
        OSClientSession<?, ?> ses = OSClientSession.getCurrent();
        Objects.requireNonNull(ses, "Unable to retrieve current session. Please verify thread has a current session available.");
        RequestBuilder<R> req = HttpRequest.builder(returnType).endpointTokenProvider(ses).config(ses.getConfig())
                .method(method).path(path);
        Map<String, String> headers = ses.getHeaders();
        if (headers != null && headers.size() > 0) {
            return new Invocation<>(req, serviceType, endpointFunc).headers(headers);
        } else {
            return new Invocation<>(req, serviceType, endpointFunc);
        }
    }

    protected int getServiceVersion() {
        OSClientSession<?, ?> session = OSClientSession.getCurrent();
        if (session.getAuthVersion() == AuthVersion.V3) {
            SortedSet<? extends Service> services = ((OSClientSession.OSClientSessionV3) session).getToken().getAggregatedCatalog().get(serviceType.getType());
            Service service = services.first();

            return service.getVersion();
        } else {
            SortedSet<? extends Access.Service> services = ((OSClientSession.OSClientSessionV2) session).getAccess().getAggregatedCatalog().get(serviceType.getType());
            Access.Service service = services.first();

            return service.getVersion();
        }
    }

    protected <T> List<T> toList(T[] arr) {
        if (arr == null)
            return Collections.emptyList();
        return Arrays.asList(arr);
    }

    protected CloudProvider getProvider() {
        return OSClientSession.getCurrent().getProvider();
    }

    protected static class Invocation<R> {
        RequestBuilder<R> req;

        protected Invocation(RequestBuilder<R> req, ServiceType serviceType, Function<String, String> endpointFunc) {
            this.req = req;
            req.serviceType(serviceType);
            req.endpointFunction(endpointFunc);
        }

        public HttpRequest<R> getRequest() {
            return req.build();
        }

        public Invocation<R> param(String name, Object value) {
            req.queryParam(name, value);
            return this;
        }

        public Invocation<R> updateParam(String name, Object value) {
            req.updateQueryParam(name, value);
            return this;
        }

        public Invocation<R> params(Map<String, ?> params) {
            if (params != null) {
                for (String name : params.keySet())
                    req.queryParam(name, params.get(name));
            }
            return this;
        }

        public Invocation<R> param(boolean condition, String name, Object value) {
            if (condition)
                req.queryParam(name, value);
            return this;
        }

        public Invocation<R> paramLists(Map<String, ? extends Iterable<?>> params) {
            if (params != null) {
                for (Map.Entry<String, ? extends Iterable<?>> pair : params.entrySet())
                    for (Object value : pair.getValue())
                        req.queryParam(pair.getKey(), value);
            }
            return this;
        }

        public Invocation<R> serviceType(ServiceType serviceType) {
            req.serviceType(serviceType);
            return this;
        }

        public Invocation<R> entity(ModelEntity entity) {
            req.entity(entity);
            return this;
        }

        public Invocation<R> entity(Payload<?> entity) {
            req.entity(entity);
            req.contentType(ClientConstants.CONTENT_TYPE_OCTECT_STREAM);
            return this;
        }

        public Invocation<R> contentType(String contentType) {
            req.contentType(contentType);
            return this;
        }

        public Invocation<R> json(String json) {
            req.json(json);
            return this;
        }

        public Invocation<R> headers(Map<String, ?> headers) {
            if (headers != null)
                req.headers(headers);
            return this;
        }

        public Invocation<R> header(String name, Object value) {
            req.header(name, value);
            return this;
        }

        public R execute() {
            return execute(null);
        }

        public R execute(ExecutionOptions<R> options) {
            header(HEADER_USER_AGENT, USER_AGENT);
            HttpRequest<R> request = req.build();
            HttpResponse res = HttpExecutor.create().execute(request);

            reqIdContainer.remove();
            reqIdContainer.set(getRequestId(res));
            return res.getEntity(request.getReturnType(), options);
        }

        public HttpResponse executeWithResponse() {
            HttpResponse res = HttpExecutor.create().execute(req.build());
            reqIdContainer.remove();
            reqIdContainer.set(getRequestId(res));
            return res;
        }

        private String getRequestId(HttpResponse res) {
            if (res.headers().containsKey(ClientConstants.X_COMPUTE_REQUEST_ID)) {
                return res.header(ClientConstants.X_COMPUTE_REQUEST_ID);
            } else {
                return res.header(ClientConstants.X_OPENSTACK_REQUEST_ID);
            }
        }
    }
}
