/**
 *
 */
package com.thinkbiganalytics.metadata.sla.spi.core;

/*-
 * #%L
 * thinkbig-sla-core
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * Licensed 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.
 * #L%
 */

import com.thinkbiganalytics.metadata.sla.api.DuplicateAgreementNameException;
import com.thinkbiganalytics.metadata.sla.api.Metric;
import com.thinkbiganalytics.metadata.sla.api.Obligation;
import com.thinkbiganalytics.metadata.sla.api.ObligationGroup;
import com.thinkbiganalytics.metadata.sla.api.ObligationGroup.Condition;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement.ID;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreementActionConfiguration;
import com.thinkbiganalytics.metadata.sla.spi.ObligationBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ObligationGroupBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementCheck;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementCheckBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider;

import org.joda.time.DateTime;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/**
 *
 */
public class InMemorySLAProvider implements ServiceLevelAgreementProvider {

    private Map<SLAID, ServiceLevelAgreement> slas;
    private Map<String, ID> nameToSlas;

    /**
     *
     */
    public InMemorySLAProvider() {
        this.slas = Collections.synchronizedMap(new HashMap<SLAID, ServiceLevelAgreement>());
        this.nameToSlas = Collections.synchronizedMap(new HashMap<String, ID>());
    }

    @Override
    public ID resolve(Serializable ser) {
        return resolveImpl(ser);
    }

    @Override
    public List<ServiceLevelAgreement> getAgreements() {
        synchronized (this.slas) {
            return new ArrayList<ServiceLevelAgreement>(this.slas.values());
        }
    }

    @Override
    public ServiceLevelAgreement getAgreement(ID id) {
        return this.slas.get(id);
    }

    @Override
    public ServiceLevelAgreement findAgreementByName(String slaName) {
        ID id = this.nameToSlas.get(slaName);

        if (id != null) {
            return this.slas.get(id);
        } else {
            return null;
        }
    }

    @Override
    public boolean removeAgreement(ID id) {
        synchronized (this.slas) {
            ServiceLevelAgreement sla = this.slas.remove(id);

            if (sla != null) {
                this.nameToSlas.remove(sla.getName());
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    public ServiceLevelAgreementBuilder builder() {
        return new SLABuilderImpl();
    }

    @Override
    public ServiceLevelAgreementBuilder builder(ID id) {
        return new SLABuilderImpl(resolveImpl(id));
    }


    private SLAImpl addSLA(SLAImpl sla) {
        synchronized (this.slas) {
            SLAID id = new SLAID();

            if (this.nameToSlas.containsKey(sla.getName())) {
                throw new DuplicateAgreementNameException(sla.getName());
            } else {
                sla.setId(id);
                this.slas.put(id, sla);
                this.nameToSlas.put(sla.getName(), id);
                return sla;
            }
        }
    }

    private SLAImpl replaceSLA(SLAID id, SLAImpl sla) {
        ServiceLevelAgreement oldSla = this.slas.get(id);
        ServiceLevelAgreement.ID namedId = this.nameToSlas.get(sla.getName());

        // Make sure the name of the new SLA does not match that of another SLA
        // besides the one being replaced.
        if (namedId == null || oldSla.getId().equals(namedId)) {
            sla.setId(id);
            this.slas.put(id, sla);
            return sla;
        } else {
            throw new DuplicateAgreementNameException(sla.getName());
        }
    }

    private SLAID resolveImpl(Serializable ser) {
        // TODO: throw unknown ID exception?
        if (ser instanceof String) {
            return new SLAID((String) ser);
        } else if (ser instanceof UUID) {
            return new SLAID((UUID) ser);
        } else if (ser instanceof SLAID) {
            return (SLAID) ser;
        } else {
            throw new IllegalArgumentException("Invalid ID source format: " + ser.getClass());
        }
    }

    @Override
    public ServiceLevelAgreementCheckBuilder slaCheckBuilder(ID slaId) {
        return null;
    }

    private static class ObligationBuilderImpl<B> implements ObligationBuilder<B> {

        private SLABuilderImpl slaBuilder;
        private ObligationGroupBuilderImpl groupBuilder;
        private ObligationGroupImpl group;
        private String description;
        private Set<Metric> metrics = new HashSet<Metric>();

        public ObligationBuilderImpl(ObligationGroupImpl group, SLABuilderImpl bldr) {
            this.slaBuilder = bldr;
            this.group = group;
        }

        public ObligationBuilderImpl(ObligationGroupImpl group, ObligationGroupBuilderImpl bldr) {
            this.groupBuilder = bldr;
            this.group = group;
        }

        @Override
        public ObligationBuilder<B> description(String descr) {
            this.description = descr;
            return this;
        }

        @Override
        public ObligationBuilder<B> metric(Metric metric, Metric... more) {
            this.metrics.add(metric);
            for (Metric another : more) {
                this.metrics.add(another);
            }
            return this;
        }

        @Override
        public ObligationBuilder<B> metric(Collection<Metric> metrics) {
            this.metrics.addAll(metrics);
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public B build() {
            ObligationImpl ob = new ObligationImpl();
            ob.description = this.description;
            ob.metrics = this.metrics;
            ob.group = this.group;
            this.group.getObligations().add(ob);

            if (this.groupBuilder != null) {
                return (B) this.groupBuilder;
            } else {
                return (B) this.slaBuilder;
            }
        }
    }

    private static class ObligationGroupBuilderImpl implements ObligationGroupBuilder {

        private SLABuilderImpl slaBuilder;
        private ObligationGroupImpl group;

        public ObligationGroupBuilderImpl(SLABuilderImpl slaBuilder, Condition cond) {
            this.slaBuilder = slaBuilder;
            this.group = new ObligationGroupImpl(this.slaBuilder.sla, cond);
        }

        @Override
        public ObligationGroupBuilder obligation(Obligation obligation) {
            this.group.getObligations().add(obligation);
            return this;
        }

        @Override
        public ObligationBuilder<ObligationGroupBuilder> obligationBuilder() {
            return new ObligationBuilderImpl<ObligationGroupBuilder>(this.group, this);
        }

        @Override
        public ServiceLevelAgreementBuilder build() {
            this.slaBuilder.sla.obligationGroups.add(this.group);
            return this.slaBuilder;
        }
    }

    private static class SLAID implements ServiceLevelAgreement.ID {

        private static final long serialVersionUID = 8914036758972637669L;

        private final UUID uuid;

        public SLAID() {
            this(UUID.randomUUID());
        }

        public SLAID(String str) {
            this(UUID.fromString(str));
        }

        public SLAID(UUID id) {
            this.uuid = id;
        }

        @Override
        public String toString() {
            return this.uuid.toString();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!this.getClass().equals(obj.getClass())) {
                return false;
            }

            return Objects.equals(this.uuid, ((SLAID) obj).uuid);
        }

        @Override
        public int hashCode() {
            return Objects.hash(getClass(), this.uuid);
        }

    }

    private static class SLAImpl implements ServiceLevelAgreement {

        boolean enabled = true;
        private ServiceLevelAgreement.ID id;
        private String name;
        private DateTime creationTime = DateTime.now();
        private String description;
        private ObligationGroupImpl defaultGroup;
        private List<ObligationGroup> obligationGroups;

        public SLAImpl() {
            this.defaultGroup = new ObligationGroupImpl(this, Condition.REQUIRED);
            this.obligationGroups = new ArrayList<ObligationGroup>();
        }

        public ServiceLevelAgreement.ID getId() {
            return id;
        }

        protected void setId(ServiceLevelAgreement.ID id) {
            this.id = id;
        }

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

        protected void setName(String name) {
            this.name = name;
        }

        @Override
        public DateTime getCreatedTime() {
            return this.creationTime;
        }

        @Override
        public String getDescription() {
            return description;
        }

        protected void setDescription(String description) {
            this.description = description;
        }

        @Override
        public boolean isEnabled() {
            return enabled;
        }

        @Override
        public List<ObligationGroup> getObligationGroups() {
            if (this.defaultGroup.getObligations().isEmpty()) {
                return this.obligationGroups;
            } else {
                ArrayList<ObligationGroup> list = new ArrayList<>();
                list.add(this.defaultGroup);
                list.addAll(this.obligationGroups);
                return list;
            }
        }

        @Override
        public List<Obligation> getObligations() {
            List<Obligation> list = new ArrayList<>();

            list.addAll(this.defaultGroup.getObligations());
            for (ObligationGroup group : this.obligationGroups) {
                list.addAll(group.getObligations());
            }

            return list;
        }

        @Override
        public List<ServiceLevelAgreementCheck> getSlaChecks() {
            return null;
        }
    }


    private static class ObligationId implements Obligation.ID {

        private static final long serialVersionUID = 8914036758972637669L;

        private final UUID uuid;

        public ObligationId() {
            this(UUID.randomUUID());
        }

        public ObligationId(String str) {
            this(UUID.fromString(str));
        }

        public ObligationId(UUID id) {
            this.uuid = id;
        }

        @Override
        public String toString() {
            return this.uuid.toString();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!this.getClass().equals(obj.getClass())) {
                return false;
            }

            return Objects.equals(this.uuid, ((SLAID) obj).uuid);
        }

        @Override
        public int hashCode() {
            return Objects.hash(getClass(), this.uuid);
        }

    }

    private static class ObligationImpl implements Obligation {

        private ObligationGroupImpl group;
        private String description;
        private Set<Metric> metrics = new HashSet<Metric>();
        private Obligation.ID id;

        public ObligationImpl() {
            this.id = new ObligationId();
        }


        @Override
        public ID getId() {
            return id;
        }

        @Override
        public String getDescription() {
            return this.description;
        }

        @Override
        public ServiceLevelAgreement getAgreement() {
            return this.group.getAgreement();
        }

        @Override
        public ObligationGroup getGroup() {
            return this.group;
        }

        @Override
        public Set<Metric> getMetrics() {
            return Collections.unmodifiableSet(this.metrics);
        }
    }

    private static class ObligationGroupImpl implements ObligationGroup {

        private SLAImpl sla;
        private Condition condition;
        private List<Obligation> obligations = new ArrayList<>();

        public ObligationGroupImpl(SLAImpl sla, Condition condition) {
            this.sla = sla;
            this.condition = condition;
        }

        @Override
        public ServiceLevelAgreement getAgreement() {
            return this.sla;
        }

        @Override
        public Condition getCondition() {
            return this.condition;
        }

        @Override
        public List<Obligation> getObligations() {
            return this.obligations;
        }
    }

    private class SLABuilderImpl implements ServiceLevelAgreementBuilder {

        private SLAID id;
        private String name;
        private String descrtion;
        private SLAImpl sla = new SLAImpl();

        public SLABuilderImpl() {
            this(null);
        }

        public SLABuilderImpl(SLAID id) {
            this.id = id;
        }

        @Override
        public ServiceLevelAgreementBuilder name(String name) {
            this.name = name;
            return this;
        }

        @Override
        public ServiceLevelAgreementBuilder description(String description) {
            this.descrtion = description;
            return this;
        }

        @Override
        public ServiceLevelAgreementBuilder obligation(Obligation obligation) {
            this.sla.getObligations().add(obligation);
            return this;
        }

        @Override
        public ObligationBuilder<ServiceLevelAgreementBuilder> obligationBuilder() {
            return new ObligationBuilderImpl<ServiceLevelAgreementBuilder>(this.sla.defaultGroup, this);
        }

        @Override
        public ObligationBuilder<ServiceLevelAgreementBuilder> obligationBuilder(Condition condition) {
            ObligationGroupImpl group = new ObligationGroupImpl(this.sla, condition);
            this.sla.obligationGroups.add(group);
            return new ObligationBuilderImpl<ServiceLevelAgreementBuilder>(group, this);
        }

        @Override
        public ObligationGroupBuilder obligationGroupBuilder(Condition condition) {
            return new ObligationGroupBuilderImpl(this, condition);
        }

        @Override
        public ServiceLevelAgreement build() {
            this.sla.setName(this.name);
            this.sla.setDescription(this.descrtion);

            if (this.id == null) {
                return addSLA(sla);
            } else {
                return replaceSLA(this.id, sla);
            }
        }

        @Override
        public ServiceLevelAgreementBuilder actionConfigurations(List<? extends ServiceLevelAgreementActionConfiguration> actionConfigurations) {
            return null;
        }
    }
}
