/*
 * 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.jackrabbit.vault.packaging.impl;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageDefinition;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
import org.apache.jackrabbit.vault.packaging.PackageManager;
import org.apache.jackrabbit.vault.packaging.Packaging;
import org.apache.jackrabbit.vault.packaging.events.impl.PackageEventDispatcher;
import org.apache.jackrabbit.vault.packaging.registry.PackageRegistry;
import org.apache.jackrabbit.vault.packaging.registry.impl.AbstractPackageRegistry;
import org.apache.jackrabbit.vault.packaging.registry.impl.CompositePackageRegistry;
import org.apache.jackrabbit.vault.packaging.registry.impl.JcrPackageRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@code PackagingImpl}...
 */
@Component(
        service = Packaging.class,
        immediate = true,
        property = {"service.vendor=The Apache Software Foundation"})
@Designate(ocd = PackagingImpl.Config.class)
public class PackagingImpl implements Packaging {

    /**
     * default logger
     */
    private static final Logger log = LoggerFactory.getLogger(PackagingImpl.class);

    @Reference
    private PackageEventDispatcher eventDispatcher;

    // In case a PackageRegistry is exposed as OSGi Service the first one will be considered
    // as base registry to fall back for dependency checks - currently only FSPackageRegistry is exposed as such
    @Reference(
            cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY)
    @SuppressWarnings("java:S3077") // volatile mandated by OSGi spec
    volatile List<PackageRegistry> registries;

    /**
     * package manager is a singleton
     */
    private final PackageManagerImpl pkgManager = new PackageManagerImpl();

    Config config;

    public PackagingImpl() {}

    @ObjectClassDefinition(name = "Apache Jackrabbit FileVault Packaging Service (Package Manager Configuration)")
    @interface Config {

        /**
         * Defines the package roots of the package manager
         */
        @AttributeDefinition(
                name = "Repository Path for Packages",
                description = "The locations in the repository which are used by the JCR package manager")
        String[] packageRoots() default {"/etc/packages"};

        @AttributeDefinition(
                name = "Allowed Users for Hook Execution",
                description =
                        "The authorizable ids or principal names which are allowed to execute hooks (in addition to 'admin', 'administrators' and 'system'")
        String[] authIdsForHookExecution();

        @AttributeDefinition(
                name = "Allowed Users for Package Installation",
                description =
                        "The authorizable ids or principal names which are allowed to install packages with the 'requireRoot' flag (in addition to 'admin', 'administrators' and 'system'")
        String[] authIdsForRootInstallation();

        @AttributeDefinition(
                name = "Default Strict Mode",
                description =
                        "Whether by default imports are executed in strict mode. In strict mode every import failure leads to an exception, while without strict mode those failures are just logged but the import continues in a best effort manner")
        boolean isStrict() default true;

        @AttributeDefinition(
                name = "Legacy Folder Primary Type Mode",
                description = "Whether to overwrite the primary type of folders during imports")
        boolean overwritePrimaryTypesOfFolders() default true;

        @AttributeDefinition(
                name = "Default ID Conflict Policy",
                description = "Default node id conflict policy to use during import")
        IdConflictPolicy defaultIdConflictPolicy() default IdConflictPolicy.FAIL;
    }

    @Activate
    private void activate(Config config) {
        this.config = config;
        pkgManager.setDispatcher(eventDispatcher);
        log.info("Jackrabbit Filevault Packaging initialized with config {}", config);
    }

    /**
     * {@inheritDoc}
     */
    public PackageManager getPackageManager() {
        return pkgManager;
    }

    /**
     * {@inheritDoc}
     */
    public JcrPackageManager getPackageManager(Session session) {
        JcrPackageManagerImpl mgr = new JcrPackageManagerImpl(
                session,
                config.packageRoots(),
                config.authIdsForHookExecution(),
                config.authIdsForRootInstallation(),
                config.isStrict(),
                config.overwritePrimaryTypesOfFolders(),
                config.defaultIdConflictPolicy());
        mgr.setDispatcher(eventDispatcher);
        setBaseRegistry(mgr.getInternalRegistry(), registries);
        return mgr;
    }

    private static boolean setBaseRegistry(
            JcrPackageRegistry jcrPackageRegistry, List<PackageRegistry> otherRegistries) {
        if (!otherRegistries.isEmpty()) {
            jcrPackageRegistry.setBaseRegistry(otherRegistries.get(0));
            return true;
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public JcrPackageDefinition createPackageDefinition(Node defNode) {
        return new JcrPackageDefinitionImpl(defNode);
    }

    /**
     * {@inheritDoc}
     */
    public JcrPackage open(Node node, boolean allowInvalid) throws RepositoryException {
        JcrPackageManager pMgr = getPackageManager(node.getSession());
        return pMgr.open(node, allowInvalid);
    }

    @Override
    public PackageRegistry getCompositePackageRegistry(Session session, boolean useJcrRegistryAsPrimaryRegistry)
            throws IOException {
        List<PackageRegistry> allRegistries = new ArrayList<>(registries);
        JcrPackageRegistry jcrPackageRegistry = getJcrPackageRegistry(session, false);
        if (useJcrRegistryAsPrimaryRegistry) {
            allRegistries.add(0, jcrPackageRegistry);
        } else {
            allRegistries.add(jcrPackageRegistry);
        }
        return new CompositePackageRegistry(allRegistries);
    }

    @Override
    public JcrPackageRegistry getJcrPackageRegistry(Session session) {
        return getJcrPackageRegistry(session, true);
    }

    @Override
    public PackageRegistry getJcrBasedPackageRegistry(Session session) {
        return getJcrPackageRegistry(session);
    }

    private JcrPackageRegistry getJcrPackageRegistry(Session session, boolean useBaseRegistry) {
        JcrPackageRegistry registry = new JcrPackageRegistry(
                session,
                new AbstractPackageRegistry.SecurityConfig(
                        config.authIdsForHookExecution(), config.authIdsForRootInstallation()),
                config.isStrict(),
                config.overwritePrimaryTypesOfFolders(),
                config.defaultIdConflictPolicy(),
                config.packageRoots());
        registry.setDispatcher(eventDispatcher);
        if (useBaseRegistry) {
            setBaseRegistry(registry, registries);
        }
        return registry;
    }
}
