/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.management.client.content;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HASH;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.jboss.as.controller.HashUtil;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.repository.ContentReference;
import org.jboss.as.repository.ContentRepository;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.Property;
import org.jboss.vfs.VirtualFile;

/**
 * {@link Resource} implementation for the root resource of a tree of resources that store managed DMR content
 * (e.g. named rollout plans.)
 *
 * @author Brian Stansberry (c) 2011 Red Hat Inc.
 */
public class ManagedDMRContentTypeResource implements Resource.ResourceEntry {

    private final PathAddress address;
    private final String childType;
    private final ContentRepository contentRepository;
    private final Map<String, ManagedContent> content = new TreeMap<String, ManagedContent>();
    private final ModelNode model = new ModelNode();
    private final MessageDigest messageDigest;

    @Deprecated
    public ManagedDMRContentTypeResource(final PathElement pathElement, final String childType,
                                         final byte[] initialHash, final ContentRepository contentRepository) {
        this(PathAddress.pathAddress(pathElement), childType, initialHash, contentRepository);
    }

    public ManagedDMRContentTypeResource(final PathAddress address, final String childType, final byte[] initialHash, final ContentRepository contentRepository) {
        this.childType = childType;
        this.contentRepository = contentRepository;
        this.address = address;

        try {
            this.messageDigest = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e) {
            throw ManagedDMRContentLogger.ROOT_LOGGER.messageDigestAlgorithmNotAvailable(e);
        }

        // Establish an initial hash attribute
        model.get(ModelDescriptionConstants.HASH);

        if (initialHash != null) {
            loadContent(initialHash);
        } // else leave attribute undefined
    }

    private ManagedDMRContentTypeResource(final ManagedDMRContentTypeResource toCopy) {
        this.childType = toCopy.childType;
        this.contentRepository = toCopy.contentRepository;
        this.messageDigest = toCopy.messageDigest;
        this.address = toCopy.address;
        synchronized (toCopy.content) {
            for (Map.Entry<String, ManagedContent> entry : toCopy.content.entrySet()) {
                ManagedContent value = entry.getValue();
                this.content.put(entry.getKey(), new ManagedContent(value.getContent(), value.getHash()));
            }
        }
        this.model.set(toCopy.model);
    }

    @Override
    public ModelNode getModel() {
        return model.clone();
    }

    @Override
    public void writeModel(ModelNode newModel) {
        // ApplyRemoteMasterDomainModelHandler is writing us
        byte[] initialHash = newModel.hasDefined(ModelDescriptionConstants.HASH) ? newModel.get(ModelDescriptionConstants.HASH).asBytes() : null;
        if (initialHash != null) {
            loadContent(initialHash);
        }
    }

    @Override
    public boolean isModelDefined() {
        return true;
    }

    @Override
    public Resource getChild(PathElement element) {
        if (hasChildren(element.getKey())) {
            synchronized (content) {
                String name = element.getValue();
                ManagedContent managedContent = content.get(name);
                if (managedContent != null) {
                    return getChildEntry(name);
                }
            }
        }
        return null;
    }

    @Override
    public Resource requireChild(PathElement address) {
        final Resource resource = getChild(address);
        if (resource == null) {
            throw new NoSuchResourceException(address);
        }
        return resource;
    }

    @Override
    public Resource navigate(PathAddress address) {
        if (address.size() == 0) {
            return this;
        } else {
            Resource child = requireChild(address.getElement(0));
            return address.size() == 1 ? child : child.navigate(address.subAddress(1));
        }
    }

    @Override
    public Set<String> getChildTypes() {
        return Collections.singleton(childType);
    }

    @Override
    public Set<Resource.ResourceEntry> getChildren(String childType) {
        if (!hasChildren(childType)) {
            return Collections.emptySet();
        } else {
            Set<Resource.ResourceEntry> result = new HashSet<ResourceEntry>();
            synchronized (content) {
                for (String name : content.keySet()) {
                    result.add(getChildEntry(name));
                }
            }
            return result;
        }
    }

    @Override
    public final boolean hasChildren(String childType) {
        return this.childType.equals(childType);
    }

    @Override
    public boolean hasChild(PathElement element) {
        return getChildrenNames(element.getKey()).contains(element.getValue());
    }

    @Override
    public Set<String> getChildrenNames(String childType) {
        if (hasChildren(childType)) {
            synchronized (content) {
                return new HashSet<String>(content.keySet());
            }
        } else {
            return Collections.emptySet();
        }
    }

    @Override
    public void registerChild(PathElement address, Resource resource) {
        if (!childType.equals(address.getKey())) {
            throw ManagedDMRContentLogger.ROOT_LOGGER.illegalChildType(address.getKey(), childType);
        }
        if (! (resource instanceof ManagedDMRContentResource)) {
            throw ManagedDMRContentLogger.ROOT_LOGGER.illegalChildClass(resource.getClass());
        }

        // Just attach ourself to this child so during the course of this operation it can access data
        ManagedDMRContentResource child = ManagedDMRContentResource.class.cast(resource);
        child.setParent(this);
    }

    @Override
    public void registerChild(PathElement address, int index, Resource resource) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Resource removeChild(PathElement address) {
        final Resource toRemove = getChild(address);
        if (toRemove != null) {
            synchronized (content) {
                content.remove(address.getValue());
            }
            try{
                storeContent();
            } catch (IOException e) {
                throw new ContentStorageException(e);
            }
        }
        return toRemove;
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public boolean isProxy() {
        return false;
    }

    @Override
    public Set<String> getOrderedChildTypes() {
        return Collections.emptySet();
    }

    @SuppressWarnings({"CloneDoesntCallSuperClone"})
    @Override
    public Resource clone() {
        return new ManagedDMRContentTypeResource(this);
    }

    @Override
    public String getName() {
        return this.address.getLastElement().getValue();
    }

    @Override
    public PathElement getPathElement() {
        return this.address.getLastElement();
    }

    ManagedContent getManagedContent(final String name) {
        return content.get(name);
    }

    byte[] storeManagedContent(final String name, final ModelNode content) throws IOException {
        final byte[] hash = hashContent(content);
        synchronized (this.content) {
            this.content.put(name, new ManagedContent(content, hash));
        }
        storeContent();
        return hash;
    }

    private void loadContent(byte[] initialHash) {
        VirtualFile vf = contentRepository.getContent(initialHash);
        if (vf == null) {
            throw ManagedDMRContentLogger.ROOT_LOGGER.noContentFoundWithHash(HashUtil.bytesToHexString(initialHash));
        }
        InputStream is = null;
        try {
            is = vf.openStream();
            ModelNode node = ModelNode.fromStream(is);
            if (node.isDefined()) {
                for (Property prop : node.asPropertyList()) {
                    ModelNode value = prop.getValue();
                    byte[] hash = hashContent(value);
                    synchronized (content) {
                        content.put(prop.getName(), new ManagedContent(value, hash));
                    }
                }
            }
            this.model.get(ModelDescriptionConstants.HASH).set(initialHash);
            contentRepository.addContentReference(new ContentReference(address.toCLIStyleString(), initialHash));
        } catch (IOException e) {
            throw new ContentStorageException(e);
        } finally {
            safeClose(is);
        }
    }

    private void storeContent() throws IOException {
        final ModelNode node = new ModelNode();
        boolean hasContent;
        ContentReference oldReference = null;
        if(this.model.hasDefined(HASH)) {
            oldReference = new ContentReference(address.toCLIStyleString(), this.model.get(HASH).asBytes());
        }
        synchronized (content) {
            hasContent = !content.isEmpty();
            if (hasContent) {
                for (Map.Entry<String, ManagedContent> entry : content.entrySet()) {
                    node.get(entry.getKey()).set(entry.getValue().content);
                }
            }
        }
        if (hasContent) {
            ByteArrayInputStream bais = new ByteArrayInputStream(node.toString().getBytes(StandardCharsets.UTF_8));
            byte[] ourHash = contentRepository.addContent(bais);
            this.model.get(HASH).set(ourHash);
            this.contentRepository.addContentReference(new ContentReference(address.toCLIStyleString(), ourHash));
        } else {
            this.model.get(HASH).clear();
        }
        if(oldReference != null) {
            this.contentRepository.removeContent(oldReference);
        }
    }

    private byte[] hashContent(ModelNode content) throws IOException {
        byte[] sha1Bytes;
        OutputStream os = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                // just discard
            }
        };
        synchronized (messageDigest) {
            messageDigest.reset();

            DigestOutputStream dos = new DigestOutputStream(os, messageDigest);
            ByteArrayInputStream bis = new ByteArrayInputStream(content.toString().getBytes(StandardCharsets.UTF_8));
            byte[] bytes = new byte[8192];
            int read;
            while ((read = bis.read(bytes)) > -1) {
                dos.write(bytes, 0, read);
            }

            sha1Bytes = messageDigest.digest();
        }
        return sha1Bytes;
    }

    private ResourceEntry getChildEntry(String name) {
        return new ManagedDMRContentResource(PathElement.pathElement(childType, name) ,this);
    }

    private static void safeClose(Closeable c) {
        if (c != null) {
            try {
                c.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    static class ManagedContent {
        private final ModelNode content;
        private final byte[] hash;

        ManagedContent(ModelNode content, byte[] hash) {
            this.content = content;
            this.hash = hash;
        }

        ModelNode getContent() {
            return content.clone();
        }

        byte[] getHash() {
            return Arrays.copyOf(hash, hash.length);
        }
    }
}
