/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.objects.service.impl;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonObject;

import ru.entaxy.platform.base.objects.EntaxyObject;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;

public class TrackedEntaxyObject implements EntaxyObject {

    private static final Logger log = LoggerFactory.getLogger(TrackedEntaxyObject.class);

    static public TrackedEntaxyObject create(BundleContext bundleContext, String objectId, String objectType) {
        log.debug("\n\t CREATING TrackedEntaxyObject: [{}]:[{}]", objectId, objectType);
        return new TrackedEntaxyObject(bundleContext, objectId, objectType);
    }

    static public TrackedEntaxyObject create(BundleContext bundleContext, String objectId, String objectType,
            String scope) {
        log.debug("\n\t CREATING TrackedEntaxyObject: [{}]:[{}]", objectId, objectType);
        return new TrackedEntaxyObject(bundleContext, objectId, objectType, scope);
    }

    protected BundleContext bundleContext;

    protected String objectId;
    protected String objectType;

    protected String displayName;

    protected String objectScope = EntaxyObject.OBJECT_SCOPE.PUBLIC;

    protected String objectOwner = "";

    protected String factoryId = "UNKNOWN";

    protected boolean isGhost = false;

    protected JsonObject configuration;

    protected BundleInfoImpl bundleInfoImpl;

    protected boolean isProvided = false;

    protected List<RelationDescriptor> relationDescriptors = new ArrayList<>();

    public static final OBJECT_RELATION DEFAULT_RELATION = OBJECT_RELATION.AGGREGATION;

    public static final OBJECT_RELATION_SCOPE DEFAULT_RELATION_SCOPE = OBJECT_RELATION_SCOPE.RUNTIME;

    public static final boolean DEFAULT_RELATION_INVERSE = true;

    protected Map<String, ObjectRelation> relations = new LinkedHashMap<>();
    protected List<Relation> incomingRelations = new ArrayList<>();

    protected TrackedEntaxyObject(BundleContext bundleContext, String objectId, String objectType) {
        super();
        this.bundleContext = bundleContext;
        this.objectId = objectId;
        this.objectType = objectType;
    }

    protected TrackedEntaxyObject(BundleContext bundleContext, String objectId, String objectType, String scope) {
        this(bundleContext, objectId, objectType);
        this.objectScope = scope;
    }

    @Override
    public String getId() {
        return this.objectId;
    }

    @Override
    public String getType() {
        return this.objectType;
    }

    @Override
    public String getDisplayName() {
        return CommonUtils.isValid(this.displayName)
                ? this.displayName
                : getId();
    }

    @Override
    public String getScope() {
        return this.objectScope;
    }

    @Override
    public String getOwner() {
        return this.objectOwner;
    }

    @Override
    public String getFactoryId() {
        return this.factoryId;
    }

    @Override
    public BundleInfo getBundleInfo() {
        return this.bundleInfoImpl;
    }

    @Override
    public String getConfiguration() {
        return (this.configuration == null ? "{}" : this.configuration.toString());
    }

    @Override
    public List<Relation> getRelations() {
        return this.relations.values()
                .stream()
                .map(r -> (Relation) r)
                .collect(Collectors.toList());
    }

    public TrackedEntaxyObject bundle(Bundle bundleValue) {
        this.bundleInfoImpl = new BundleInfoImpl(bundleContext, bundleValue.getBundleId());
        return this;
    };

    public TrackedEntaxyObject configuration(String configurationValue) {

        try {
            this.configuration = JSONUtils.getJsonRootObject(configurationValue);
            if (this.configuration.has(EntaxyObject.FIELDS.FACTORY_ID))
                this.factoryId = this.configuration.get(EntaxyObject.FIELDS.FACTORY_ID).getAsString();
        } catch (Exception e) {

        }

        if (this.configuration == null)
            this.configuration = new JsonObject();

        return this;
    }

    @Override
    public boolean isProvided() {
        return isProvided;
    }

    public void setProvided(boolean isProvided) {
        this.isProvided = isProvided;
    };

    @Override
    public boolean isComplete() {
        return (this.bundleContext != null) && (this.configuration != null);
    }

    public void relation(TrackedEntaxyObject target, RelationDescriptor descriptor) {
        ObjectRelation rel = new ObjectRelation();
        rel.source = this;
        rel.target = target;
        rel.type = descriptor.relationType;
        rel.scope = descriptor.relationScope;
        rel.name = descriptor.relationName;
        rel.group = descriptor.relationGroup;
        rel.isDefinedInSource = !descriptor.relationInverse;
        this.relations.put(rel.getUniqueId(), rel);
        target.incomingRelation(rel);
    }

    public void removeRelation(ObjectRelation relation) {
        this.relations.remove(relation.getUniqueId());
    }

    public void incomingRelation(Relation relation) {
        this.incomingRelations.add(relation);
    }

    public void removeIncomingRelation(Relation relation) {
        this.incomingRelations.remove(relation);
    }

    public boolean hasRelations() {
        return !this.relations.isEmpty() || !this.incomingRelations.isEmpty();
    }

    @Override
    public List<Relation> getIncomingRelations() {
        return incomingRelations;
    }

    public void relationDescription(String descriptor) {
        RelationDescriptor rd = createRelationDescription(descriptor);
        String message = "\n\tPARSING: ".concat(descriptor).concat("; RESULT: ");
        if (rd != null) {
            this.relationDescriptors.add(rd);
            message = message.concat(rd.toString());
        } else {
            message = message.concat("null");
        }
        log.debug(message);
    }

    protected RelationDescriptor createRelationDescription(String descriptor) {
        String[] parsed = descriptor.split(":");
        RelationDescriptor rd = new RelationDescriptor();

        if (parsed.length < 2)
            return null;

        rd.targetId = parsed[0];
        rd.targetType = parsed[1];

        if (parsed.length < 3)
            return rd;
        rd.relationName = CommonUtils.getValid(parsed[2], "relation");


        if (parsed.length < 4)
            return rd;
        if (!parseRelationType(parsed[3], rd)) {
            // old-style relation descriptor without relation type

            rd.relationType = DEFAULT_RELATION;
            rd.relationInverse = DEFAULT_RELATION_INVERSE;

            rd.relationGroup = CommonUtils.getValid(parsed[3], "");
            return rd;
        }

        if (parsed.length < 5)
            return rd;
        rd.relationGroup = CommonUtils.getValid(parsed[4], "");

        return rd;
    }

    protected boolean parseRelationType(String data, RelationDescriptor rd) {
        if (!CommonUtils.isValid(data))
            return false;
        boolean inverse = data.startsWith("-");
        String type = inverse ? data.substring(1) : data;

        String typeToParse = type;
        String scopeToParse = "";
        int ind = type.indexOf("/");
        if (ind > 0) {
            typeToParse = type.substring(0, ind);
            scopeToParse = type.substring(ind + 1);
        }

        try {
            rd.relationType = OBJECT_RELATION.valueOf(typeToParse.toUpperCase());
            rd.relationInverse = inverse;
        } catch (Exception e) {
            return false;
        }

        try {
            rd.relationScope = OBJECT_RELATION_SCOPE.valueOf(scopeToParse.toUpperCase());
        } catch (Exception e) {
            // ignore: use default scope
        }

        return true;
    }

    public List<RelationDescriptor> getRelationDescriptors() {
        return relationDescriptors;
    }

    public void setRelationDescriptors(List<RelationDescriptor> relationDescriptors) {
        this.relationDescriptors = relationDescriptors;
    }

    public void updateFrom(TrackedEntaxyObject newOne) {
        this.objectScope = newOne.getScope();
        this.objectOwner = newOne.getOwner();
        this.bundleContext = newOne.bundleContext;
        this.bundle(this.bundleContext.getBundle(newOne.getBundleInfo().getBundleId()));
        this.configuration = newOne.configuration;
        this.factoryId = newOne.factoryId;
        this.displayName = newOne.displayName;
        this.setProvided(newOne.isProvided);
        this.setRelationDescriptors(newOne.getRelationDescriptors());
        this.relations.putAll(newOne.relations);
    }

    public void clear() {
        this.objectScope = "";
        this.objectOwner = "";
        this.displayName = null;
        this.bundleContext = null;
        this.bundleInfoImpl = null;
        this.configuration = null;
        this.factoryId = "";
        this.setProvided(false);
    }

    public void makeGhost() {
        this.isGhost = true;
        List<String> idsToRemove = new ArrayList<>();

        for (ObjectRelation rel : this.relations.values()) {
            if (rel.isDefinedInSource()) {
                idsToRemove.add(rel.getUniqueId());
                rel.target.removeIncomingRelation((Relation) rel);
            }
        }
        for (String id : idsToRemove)
            this.relations.remove(id);

        List<Relation> relationsToRemove = new ArrayList<>();
        for (Relation rel : this.incomingRelations) {
            if (!rel.isDefinedInSource()) {
                if (rel instanceof ObjectRelation) {
                    ((ObjectRelation) rel).source.removeRelation((ObjectRelation) rel);
                    relationsToRemove.add(rel);
                }
            }
        }

        for (Relation rel : relationsToRemove)
            this.incomingRelations.remove(rel);
    }

    public void unmakeGhost() {
        this.isGhost = false;
    }

    @Override
    public boolean isGhost() {
        return isGhost;
    }

    public TrackedEntaxyObject scope(String scope) {
        this.objectScope = scope;
        return this;
    }

    public TrackedEntaxyObject owner(String owner) {
        this.objectOwner = owner;
        return this;
    }

    public TrackedEntaxyObject displayName(String displayName) {
        this.displayName = displayName;
        return this;
    }


    protected static class RelationDescriptor {

        String targetId;
        String targetType;

        String relationName = "relation";
        OBJECT_RELATION relationType = DEFAULT_RELATION;
        boolean relationInverse = false;
        String relationGroup = "";

        OBJECT_RELATION_SCOPE relationScope = DEFAULT_RELATION_SCOPE;

        @Override
        public String toString() {
            return targetId.concat(":")
                    .concat(targetType).concat(":")
                    .concat(relationName).concat(":")
                    .concat(relationInverse ? "-" : "")
                    .concat(relationType.name()).concat("/").concat(relationScope.name()).concat(":")
                    .concat(relationGroup);
        }

    }

    protected static class ObjectRelation implements Relation {

        TrackedEntaxyObject source;
        TrackedEntaxyObject target;
        OBJECT_RELATION type;
        String name;
        String group;

        OBJECT_RELATION_SCOPE scope;

        boolean isDefinedInSource = true;

        @Override
        public EntaxyObject getSource() {
            return source;
        }

        @Override
        public EntaxyObject getTarget() {
            return target;
        }

        @Override
        public OBJECT_RELATION getType() {
            return type;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getGroup() {
            return group;
        }

        @Override
        public OBJECT_RELATION_SCOPE getScope() {
            return scope;
        }

        @Override
        public boolean isDefinedInSource() {
            return isDefinedInSource;
        }

        public String getUniqueId() {
            return type.name().concat("/").concat(scope.name()).concat(":").concat(name).concat(":").concat(group)
                    .concat(":").concat(target.getId());
        }
    }
}
