/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.viewer.restfulobjects.viewer.resources;

import java.util.function.UnaryOperator;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.springframework.stereotype.Component;

import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.causeway.core.metamodel.util.Facets;
import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.causeway.viewer.restfulobjects.applib.Rel;
import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
import org.apache.causeway.viewer.restfulobjects.applib.RestfulMediaType;
import org.apache.causeway.viewer.restfulobjects.applib.RestfulResponse.HttpStatusCode;
import org.apache.causeway.viewer.restfulobjects.applib.domaintypes.DomainTypeResource;
import org.apache.causeway.viewer.restfulobjects.applib.util.UrlEncodingUtils;
import org.apache.causeway.viewer.restfulobjects.rendering.Caching;
import org.apache.causeway.viewer.restfulobjects.rendering.LinkBuilder;
import org.apache.causeway.viewer.restfulobjects.rendering.Responses;
import org.apache.causeway.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ActionDescriptionReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ActionParameterDescriptionReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.CollectionDescriptionReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.DomainTypeReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ParentSpecAndAction;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ParentSpecAndActionParam;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ParentSpecAndCollection;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.ParentSpecAndProperty;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.PropertyDescriptionReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.TypeActionResultReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.domaintypes.TypeListReprRenderer;
import org.apache.causeway.viewer.restfulobjects.rendering.service.RepresentationService;
import org.apache.causeway.viewer.restfulobjects.rendering.util.RequestParams;
import org.apache.causeway.viewer.restfulobjects.viewer.util.UrlParserUtils;

import org.jspecify.annotations.NonNull;

import lombok.extern.log4j.Log4j2;

/**
 * Implementation note: it seems to be necessary to annotate the implementation
 * with {@link Path} rather than the interface (at least under RestEasy 1.0.2
 * and 1.1-RC2).
 */
@Component
@Path("/domain-types")
@Log4j2
public class DomainTypeResourceServerside
extends ResourceAbstract
implements DomainTypeResource {

    public DomainTypeResourceServerside() {
        super();
        log.debug("<init>");
    }

    @Override
    @GET
    @Path("/")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_TYPE_LIST })
    public Response domainTypes() {

        var resourceContext = createResourceContext(
                RepresentationType.TYPE_LIST, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var domainTypeSpecifications = getSpecificationLoader().snapshotSpecifications()
                .filter(spec->spec.isEntityOrViewModel()); // concrete types only, no abstract types

        final TypeListReprRenderer renderer =
                new TypeListReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(domainTypeSpecifications).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/",
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_DOMAIN_TYPE })
    public Response domainType(
            @PathParam("domainType") final String domainType) {

        var resourceContext = createResourceContext(
                RepresentationType.DOMAIN_TYPE, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var objectSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        if(objectSpec==null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}", domainType,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        var renderer = new DomainTypeReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(objectSpec).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/{}", domainType,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}/layout")
    @Produces({
        MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_LAYOUT_BS,
        MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_LAYOUT_BS
    })
    public Response layout(
            @PathParam("domainType") final String domainType) {

        var resourceContext = createResourceContext(
                RepresentationType.LAYOUT, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var serializationStrategy = resourceContext.getSerializationStrategy();

        var responseBuilder = getSpecificationLoader().specForLogicalTypeName(domainType)
                .map(Facets::bootstrapGrid)
                .map(grid ->
                        Response.status(Response.Status.OK)
                                .entity(serializationStrategy.entity(grid))
                                .type(serializationStrategy.type(RepresentationType.LAYOUT)))
                .orElse(Responses.ofNotFound());

        return _EndpointLogging.response(log, "GET({}) /domain-types/{}/layout", serializationStrategy.name(), domainType,
                responseBuilder.build());
    }

    @Override
    @GET
    @Path("/{domainType}/properties/{propertyId}")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_PROPERTY_DESCRIPTION })
    public Response typeProperty(
            @PathParam("domainType") final String domainType,
            @PathParam("propertyId") final String propertyId) {

        var resourceContext = createResourceContext(
                RepresentationType.PROPERTY_DESCRIPTION, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var parentSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        if (parentSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/properties/{}", domainType, propertyId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        var objectMember = parentSpec.getAssociation(propertyId)
                .orElseThrow(()->
                    _EndpointLogging.error(log, "GET /domain-types/{}/properties/{}", domainType, propertyId,
                        RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND)));

        if (objectMember.isOneToManyAssociation()) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/properties/{}", domainType, propertyId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }
        final OneToOneAssociation property = (OneToOneAssociation) objectMember;

        final PropertyDescriptionReprRenderer renderer = new PropertyDescriptionReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(new ParentSpecAndProperty(parentSpec, property)).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/{}/properties/{}", domainType, propertyId,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}/collections/{collectionId}")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_COLLECTION_DESCRIPTION })
    public Response typeCollection(
            @PathParam("domainType") final String domainType,
            @PathParam("collectionId") final String collectionId) {

        var resourceContext = createResourceContext(
                RepresentationType.COLLECTION_DESCRIPTION, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var parentSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        if (parentSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/collections/{}", domainType, collectionId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        var objectMember = parentSpec.getAssociation(collectionId)
                .orElseThrow(()->
                    _EndpointLogging.error(log, "GET /domain-types/{}/collections/{}", domainType, collectionId,
                            RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND)));

        if (objectMember.isOneToOneAssociation()) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/collections/{}", domainType, collectionId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }
        final OneToManyAssociation collection = (OneToManyAssociation) objectMember;

        final CollectionDescriptionReprRenderer renderer = new CollectionDescriptionReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(new ParentSpecAndCollection(parentSpec, collection)).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/{}/collections/{}", domainType, collectionId,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}/actions/{actionId}")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_ACTION_DESCRIPTION })
    public Response typeAction(
            @PathParam("domainType") final String domainType,
            @PathParam("actionId") final String actionId) {

        var resourceContext = createResourceContext(
                RepresentationType.ACTION_DESCRIPTION, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var parentSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        if (parentSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/actions/{}", domainType, actionId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        var action = parentSpec.getAction(actionId)
                .orElseThrow(()->_EndpointLogging.error(log, "GET /domain-types/{}/actions/{}", domainType, actionId,
                        RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND)));

        final ActionDescriptionReprRenderer renderer = new ActionDescriptionReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(new ParentSpecAndAction(parentSpec, action)).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/{}/actions/{}", domainType, actionId,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}/actions/{actionId}/params/{paramId}")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_ACTION_PARAMETER_DESCRIPTION })
    public Response typeActionParam(
            @PathParam("domainType") final String domainType,
            @PathParam("actionId") final String actionId,
            @PathParam("paramId") final String paramId) {

        var resourceContext = createResourceContext(
                RepresentationType.ACTION_PARAMETER_DESCRIPTION, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);

        var parentSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        if (parentSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/actions/{}/params/{}", domainType, actionId, paramId,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        var parentAction = parentSpec.getAction(actionId)
                .orElseThrow(()->_EndpointLogging.error(log, "GET /domain-types/{}/actions/{}/params/{}", domainType, actionId, paramId,
                        RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND)));

        final ObjectActionParameter actionParam = parentAction.getParameterById(paramId);

        final ActionParameterDescriptionReprRenderer renderer = new ActionParameterDescriptionReprRenderer(resourceContext, null, JsonRepresentation.newMap());
        renderer.with(new ParentSpecAndActionParam(parentSpec, actionParam)).includesSelf();

        return _EndpointLogging.response(log, "GET /domain-types/{}/actions/{}/params/{}", domainType, actionId, paramId,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    // //////////////////////////////////////////////////////////
    // domain type actions
    // //////////////////////////////////////////////////////////

    @Override
    @GET
    @Path("/{domainType}/type-actions/isSubtypeOf/invoke")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_TYPE_ACTION_RESULT,
        RestfulMediaType.APPLICATION_JSON_ERROR })
    public Response domainTypeIsSubtypeOf(
            @PathParam("domainType") final String domainType,
            @QueryParam("supertype") final String superTypeStr, // simple style
            @QueryParam("args") final String argsUrlEncoded // formal style
            ) {

        var resourceContext = createResourceContext(
                ResourceDescriptor.generic(Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE));

        final String supertype = domainTypeFor(superTypeStr, argsUrlEncoded, "supertype",
                roEx->_EndpointLogging.error(log, "GET /domain-types/{}/type-actions/isSubtypeOf/invoke", domainType, roEx));

        var domainTypeSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        var supertypeSpec = getSpecificationLoader().specForLogicalTypeName(supertype).orElse(null);
        if (domainTypeSpec == null
                || supertypeSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/type-actions/isSubtypeOf/invoke", domainType,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        final TypeActionResultReprRenderer renderer = new TypeActionResultReprRenderer(resourceContext, null, JsonRepresentation.newMap());

        final String url = "domain-types/" + domainType + "/type-actions/isSubtypeOf/invoke";
        final LinkBuilder linkBuilder = LinkBuilder.newBuilder(resourceContext, Rel.SELF.getName(), RepresentationType.TYPE_ACTION_RESULT, url);
        final JsonRepresentation arguments = DomainTypeReprRenderer.argumentsTo(resourceContext, "supertype", supertypeSpec);
        final JsonRepresentation selfLink = linkBuilder.withArguments(arguments).build();

        final boolean value = domainTypeSpec.isOfType(supertypeSpec);
        renderer.with(domainTypeSpec).withSelf(selfLink).withValue(value);

        return _EndpointLogging.response(log, "GET /domain-types/{}/type-actions/isSubtypeOf/invoke", domainType,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    @Override
    @GET
    @Path("/{domainType}/type-actions/isSupertypeOf/invoke")
    @Produces({
        MediaType.APPLICATION_JSON,
        RestfulMediaType.APPLICATION_JSON_TYPE_ACTION_RESULT,
        RestfulMediaType.APPLICATION_JSON_ERROR })
    public Response domainTypeIsSupertypeOf(
            @PathParam("domainType") final String domainType,
            @QueryParam("subtype") final String subTypeStr, // simple style
            @QueryParam("args") final String argsUrlEncoded // formal style
            ) {

        var resourceContext = createResourceContext(
                ResourceDescriptor.generic(Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE));

        final String subtype = domainTypeFor(subTypeStr, argsUrlEncoded, "subtype",
                roEx->_EndpointLogging.error(log, "GET /domain-types/{}/type-actions/isSupertypeOf/invoke", domainType, roEx));

        var domainTypeSpec = getSpecificationLoader().specForLogicalTypeName(domainType).orElse(null);
        var subtypeSpec = getSpecificationLoader().specForLogicalTypeName(subtype).orElse(null);
        if (domainTypeSpec == null
                || subtypeSpec == null) {
            throw _EndpointLogging.error(log, "GET /domain-types/{}/type-actions/isSupertypeOf/invoke", domainType,
                    RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND));
        }

        final TypeActionResultReprRenderer renderer = new TypeActionResultReprRenderer(resourceContext, null, JsonRepresentation.newMap());

        final String url = "domain-types/" + domainType + "/type-actions/isSupertypeOf/invoke";
        final LinkBuilder linkBuilder = LinkBuilder.newBuilder(resourceContext, Rel.SELF.getName(), RepresentationType.TYPE_ACTION_RESULT, url);
        final JsonRepresentation arguments = DomainTypeReprRenderer.argumentsTo(resourceContext, "subtype", subtypeSpec);
        final JsonRepresentation selfLink = linkBuilder.withArguments(arguments).build();

        final boolean value = subtypeSpec.isOfType(domainTypeSpec);
        renderer.with(domainTypeSpec).withSelf(selfLink).withValue(value);

        return _EndpointLogging.response(log, "GET /domain-types/{}/type-actions/isSupertypeOf/invoke", domainType,
                Responses.ofOk(renderer, Caching.ONE_DAY).build());
    }

    private static String domainTypeFor(
            final String domainTypeStr,
            final String argsAsUrlEncodedQueryString,
            final String argsParamId,
            final @NonNull UnaryOperator<RestfulObjectsApplicationException> onRoException) {

        // simple style; simple return
        if (!_Strings.isNullOrEmpty(domainTypeStr)) {
            return domainTypeStr;
        }

        // formal style; must parse from args that has a link with an href to the domain type
        var requestParams = RequestParams.ofQueryString(UrlEncodingUtils.urlDecode(argsAsUrlEncodedQueryString));
        final String href = linkFromFormalArgs(requestParams, argsParamId, onRoException);
        return UrlParserUtils.domainTypeFrom(href);
    }

    private static String linkFromFormalArgs(
            final RequestParams requestParams,
            final String paramId,
            final @NonNull UnaryOperator<RestfulObjectsApplicationException> onRoException) {
        final JsonRepresentation arguments = requestParams.asMap();
        if (!arguments.isLink(paramId)) {
            throw onRoException.apply(RestfulObjectsApplicationException
                    .createWithMessage(HttpStatusCode.BAD_REQUEST, "Args should contain a link '%s'", paramId));
        }

        return arguments.getLink(paramId).getHref();
    }

}
