package com.atlassian.stash.internal.auth.trusted;

import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.bitbucket.util.CustomPreconditions;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.codec.binary.Base64;
import org.hibernate.Hibernate;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.util.Chainable.chain;

@Entity
@Cacheable
@Table(name = InternalTrustedApplication.TABLE)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class InternalTrustedApplication {

    private static final String ID_GEN = "trustedAppIdGenerator";

    static final String TABLE = "trusted_app";

    @TableGenerator(name=ID_GEN, table= ApplicationConstants.ID_SEQUENCE_TABLE, pkColumnValue = TABLE, allocationSize = 10)
    @Id
    @GeneratedValue(generator = ID_GEN, strategy = GenerationType.TABLE)
    private final Integer id;

    @Column(name = "application_id", nullable = false, unique = true) @RequiredString(size = 255)
    private final String applicationId;

    @Column(name = "public_key_base64", nullable = false) @RequiredString(size = 4000)
    private final String publicKeyBase64;

    @Transient
    private transient volatile byte[] publicKey;

    // use a lazy fetch, as otherwise Hibernate uses an outer join to fetch each trusted app and its restrictions,
    // when using session().createCriteria(...).list(). As a result, it returns a list with duplicate trusted apps
    // (one duplicate for each restriction) in that case. To reproduce, run TrustedApplicationsDaoImplTest.testFindAll()
    @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private final Set<InternalTrustedApplicationRestriction> restrictions;

    @Column(name = "certificate_timeout", nullable = false)
    private final long certificateTimeout;

    // for hibernate
    @SuppressWarnings("UnusedDeclaration")
    protected InternalTrustedApplication() {
        id = null;
        applicationId = null;
        publicKeyBase64 = null;
        certificateTimeout = 1000L;
        restrictions = Collections.emptySet();
    }

    private InternalTrustedApplication(Builder builder) {

        this.id = builder.id;
        this.applicationId = builder.applicationId;
        this.publicKey = builder.publicKey;
        this.publicKeyBase64 = new String(Base64.encodeBase64(publicKey));
        this.certificateTimeout = builder.certificateTimeout;

        ImmutableSet.Builder<InternalTrustedApplicationRestriction> restrictionBuilder = ImmutableSet.builder();
        for (InternalTrustedApplicationRestriction restriction : builder.restrictions) {
            // create copies to pass in the this reference
            restrictionBuilder.add(new InternalTrustedApplicationRestriction(
                    restriction.getId(), this, restriction.getType(), restriction.getValue()));
        }
        this.restrictions = restrictionBuilder.build();

        CustomPreconditions.checkRequiredString(applicationId, 255);
        CustomPreconditions.checkRequiredString(publicKeyBase64, 4000);
    }

    public static InternalTrustedApplication initialize(InternalTrustedApplication app) {
        if (app != null) {
            app.initialize();
        }
        return app;
    }

    protected void initialize() {
        Hibernate.initialize(getRestrictions());
        for (InternalTrustedApplicationRestriction restriction : getRestrictions()) {
            Hibernate.initialize(restriction);
        }
    }

    public Integer getId() {
        return id;
    }

    public String getApplicationId() {
        return applicationId;
    }

    public byte[] getPublicKey() {
        if (publicKey == null) {
            publicKey = Base64.decodeBase64(publicKeyBase64.getBytes());
        }
        return publicKey;
    }

    public Set<InternalTrustedApplicationRestriction> getRestrictions() {
        return restrictions;
    }

    public Iterable<String> getIPPatterns() {
        return chain(getRestrictions())
                .filter(byType(InternalTrustedApplicationRestriction.Type.IP))
                .transform(restrictionToString());
    }

    public Iterable<String> getUrlPatterns() {
        return chain(getRestrictions())
                .filter(byType(InternalTrustedApplicationRestriction.Type.URL))
                .transform(restrictionToString());
    }

    public long getCertificateTimeout() {
        return certificateTimeout;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof InternalTrustedApplication) {
            InternalTrustedApplication that = (InternalTrustedApplication) o;

            return Objects.equals(getApplicationId(), that.getApplicationId());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getApplicationId());
    }

    public Builder copy() {
        return new Builder(this);
    }

    private Predicate<? super InternalTrustedApplicationRestriction> byType(
            final InternalTrustedApplicationRestriction.Type type) {
        return input -> input.getType() == type;
    }

    private Function<InternalTrustedApplicationRestriction, String> restrictionToString() {
        return InternalTrustedApplicationRestriction::getValue;
    }

    public static class Builder {
        private final Set<InternalTrustedApplicationRestriction> restrictions;
        private String applicationId;
        private long certificateTimeout;
        private Integer id;
        private byte[] publicKey;

        public Builder() {
            restrictions = Sets.newHashSet();
        }

        public Builder(InternalTrustedApplication trustedApp) {
            applicationId = trustedApp.getApplicationId();
            certificateTimeout = trustedApp.getCertificateTimeout();
            id = trustedApp.getId();
            publicKey = trustedApp.getPublicKey();
            restrictions = Sets.newHashSet(trustedApp.getRestrictions());
        }

        public Builder id(Integer id) {
            this.id = id;
            return this;
        }

        public Builder applicationId(String applicationId) {
            this.applicationId = applicationId;
            return this;
        }

        public Builder publicKey(byte[] publicKey) {
            this.publicKey = publicKey;
            return this;
        }

        public Builder certificateTimeout(long certificateTimeout) {
            this.certificateTimeout = certificateTimeout;
            return this;
        }

        public Builder ipPatterns(Iterable<String> ipPatterns) {
            setRestrictions(InternalTrustedApplicationRestriction.Type.IP, ipPatterns);
            return this;
        }

        public Builder urlPatterns(Iterable<String> urlPatterns) {
            setRestrictions(InternalTrustedApplicationRestriction.Type.URL, urlPatterns);
            return this;
        }

        public InternalTrustedApplication build() {
            return new InternalTrustedApplication(this);
        }

        private void setRestrictions(InternalTrustedApplicationRestriction.Type type, Iterable<String> values) {
            Set<String> newValues = Sets.newHashSet(values);
            Iterator<InternalTrustedApplicationRestriction> it = restrictions.iterator();
            while (it.hasNext()) {
                InternalTrustedApplicationRestriction restriction = it.next();
                if (restriction.getType() == type) {
                    if (!newValues.remove(restriction.getValue())) {
                        // newValues did not contain restriction
                        it.remove();
                    }
                }
            }

            for (String value : newValues) {
                restrictions.add(new InternalTrustedApplicationRestriction(null, type, value));
            }
        }
    }

}
