/*
 *  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.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.causeway.viewer.restfulobjects.applib.RestfulResponse;
import org.apache.causeway.viewer.restfulobjects.rendering.IResourceContext;
import org.apache.causeway.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
import org.apache.causeway.viewer.restfulobjects.rendering.service.valuerender.JsonValueEncoderService;
import org.apache.causeway.viewer.restfulobjects.rendering.util.RequestParams;

/**
 * Utility class that encapsulates the logic for parsing some content (JSON, or a simple string that is JSON)
 * into an{@link ManagedObject} of a specified
 * {@link ObjectSpecification type}.
 */
public class JsonParserHelper {

    private static final Pattern OBJECT_OID = Pattern.compile(".*objects\\/([^/]+)\\/(.+)");

    private final IResourceContext resourceContext;
    private final ObjectSpecification objectSpec;
    private final JsonValueEncoderService jsonValueEncoder;

    public JsonParserHelper(final IResourceContext resourceContext, final ObjectSpecification objectSpecification) {
        this.objectSpec = objectSpecification;
        this.resourceContext = resourceContext;
        this.jsonValueEncoder = resourceContext.getMetaModelContext().getServiceRegistry()
                .lookupServiceElseFail(JsonValueEncoderService.class);
    }

    /**
     * @param bodyAsString
     *            - as per {@link org.apache.causeway.viewer.restfulobjects.rendering.util.Util#asStringUtf8(java.io.InputStream)}
     * @return
     */
    ManagedObject parseAsMapWithSingleValue(final RequestParams body) {
        final JsonRepresentation arguments = body.asMap();
        return parseAsMapWithSingleValue(arguments);
    }

    ManagedObject parseAsMapWithSingleValue(final JsonRepresentation arguments) {
        final JsonRepresentation representation = arguments.getRepresentation("value");
        if (arguments.size() != 1 || representation == null) {
            throw RestfulObjectsApplicationException.createWithMessage(RestfulResponse.HttpStatusCode.BAD_REQUEST, "Body should be a map with a single key 'value' whose value represents an instance of type '%s'", resourceFor(objectSpec));
        }

        return objectAdapterFor(arguments);
    }

    /**
     *
     * @param argRepr
     *            - expected to be either a String or a Map (ie from within a
     *            List, built by parsing a JSON structure).
     */
    ManagedObject objectAdapterFor(final JsonRepresentation argRepr) {

        if (argRepr == null) {
            return null;
        }

        if(!argRepr.mapHas("value")) {
            String reason = "No 'value' key";
            argRepr.mapPutString("invalidReason", reason);
            throw new IllegalArgumentException(reason);
        }

        if (objectSpec == null) {
            String reason = "ObjectSpec is null, cannot validate";
            argRepr.mapPutString("invalidReason", reason);
            throw new IllegalArgumentException(reason);
        }

        final JsonRepresentation argValueRepr = argRepr.getRepresentation("value");

        // value (encodable)
        if (objectSpec.isValue()) {
            try {
                return jsonValueEncoder.asAdapter(objectSpec, argValueRepr, null);
            } catch(IllegalArgumentException ex) {
                argRepr.mapPutString("invalidReason", ex.getMessage());
                throw ex;
            } catch(Exception ex) {
                StringBuilder buf = new StringBuilder("Failed to parse representation ");
                try {
                    final String reprStr = argRepr.getString("value");
                    buf.append("'").append(reprStr).append("' ");
                } catch(Exception ex2) {
                }
                buf.append("as value of type '").append(objectSpec.getShortIdentifier()).append("'");
                String reason = buf.toString();
                argRepr.mapPutString("invalidReason", reason);
                throw new IllegalArgumentException(reason);
            }
        }

        // reference
        if (!argValueRepr.isLink()) {
            final String reason = "Expected a link (because this object's type is not a value) but found no 'href'";
            argRepr.mapPutString("invalidReason", reason);
            throw new IllegalArgumentException(reason);
        }
        final String oidFromHref = encodedOidFromLink(argValueRepr);
        if (oidFromHref == null) {
            final String reason = "Could not parse 'href' to identify the object's OID";
            argRepr.mapPutString("invalidReason", reason);
            throw new IllegalArgumentException(reason);
        }

        var objectAdapter = resourceContext.getObjectAdapterForOidFromHref(oidFromHref)
                .orElseThrow(()->{
                    var reason = "'href' does not reference a known entity";
                    argRepr.mapPutString("invalidReason", reason);
                    return new IllegalArgumentException(reason);
                });
        return objectAdapter;
    }

    static String encodedOidFromLink(final JsonRepresentation link) {
        final String href = link.getString("href");

        final Matcher matcher = OBJECT_OID.matcher(href);
        if (!matcher.matches()) {
            return null;
        }
        String domainType = matcher.group(1);
        String instanceId = matcher.group(2);

        return Bookmark.forLogicalTypeNameAndIdentifier(domainType, instanceId).stringify();
    }

    private static String resourceFor(final ObjectSpecification objectSpec) {
        // TODO: should return a string in the form
        // http://localhost:8080/types/xxx
        return objectSpec.getFullIdentifier();
    }

}
