package com.atlassian.bitbucket.mesh.git.ref;

import com.atlassian.bitbucket.mesh.rpc.v1.RpcAnnotatedTag;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcPerson;
import com.atlassian.bitbucket.mesh.util.Person;
import com.google.protobuf.Timestamp;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.atlassian.bitbucket.mesh.rpc.util.ByteStringUtils.nullToEmpty;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.abbreviate;

public class AnnotatedTag extends Tag {

    /**
     * A placeholder {@link RpcPerson#getName name} used when a tag's {@code tagger} can't be parsed.
     */
    public static final String UNSPECIFIED_NAME = "Unspecified";

    // Example: C O Mitter <committer@example.com> 1465981006 +0000
    private static final Pattern TAGGER_PATTERN =
            Pattern.compile("(?<taggerUsername>.*) <(?<taggerEmail>.*)> (?<taggerTimestamp>\\d+) [+-]\\d+");
    private static final Logger log = LoggerFactory.getLogger(AnnotatedTag.class);

    private final String message;
    private final Person tagger;
    private final Instant taggerTimestamp;

    private AnnotatedTag(@Nonnull Builder builder) {
        super(builder);

        requireNonNull(super.getHash(), "hash");

        message = builder.message;
        tagger = builder.tagger;
        taggerTimestamp = builder.taggerTimestamp;
    }

    @Nonnull
    public Optional<String> getMessage() {
        return Optional.ofNullable(message);
    }

    public Person getTagger() {
        return tagger;
    }

    public Instant getTaggerTimestamp() {
        return taggerTimestamp;
    }

    @Nonnull
    @Override
    public String getHash() {
        //noinspection ConstantConditions - enforced in constructor
        return super.getHash();
    }

    /**
     * Parses the {@code tagger} header from an annotated tag's metadata to produce a {@link RpcPerson} for the
     * name and e-mail address it contains.
     *
     * @param tagger the tagger output to parse
     * @return the parsed tagger, or an {@link #UNSPECIFIED_NAME unspecified placeholder} if header contains
     *         incomplete or unexpected data
     */
    @Nonnull
    public static Person parseTagger(@Nonnull String tagger) {
        Matcher matcher = TAGGER_PATTERN.matcher(tagger);
        if (matcher.matches()) {
            String username = StringUtils.defaultIfBlank(matcher.group("taggerUsername"), UNSPECIFIED_NAME);
            String email = StringUtils.defaultIfBlank(matcher.group("taggerEmail"), null);
            return new Person(username, email);
        }

        log.warn("Could not parse tagger from '{}'", tagger);
        return new Person(UNSPECIFIED_NAME, null);
    }

    /**
     * Parses the {@code tagger} header from an annotated tag's metadata to attempt to calculate when the tag was
     * created.
     *
     * @param tagger the tagger output to parse
     * @return the successfully parsed tagger timestamp or {@code null} if we cannot parse the {@code tagger}
     */
    public static Instant parseTaggerTimestamp(@Nonnull String tagger) {
        Matcher matcher = TAGGER_PATTERN.matcher(tagger);
        if (matcher.matches()) {
            String timestamp = StringUtils.defaultIfBlank(matcher.group("taggerTimestamp"), null);
            if (StringUtils.isNotBlank(timestamp)) {
                try {
                    return Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(Long.parseLong(timestamp)));
                } catch (NumberFormatException e) {
                    log.warn("Could not parse the tagger timestamp from '{}'", timestamp);
                    return null;
                }
            }
        }

        log.warn("Could not parse the tagger timestamp from '{}'", tagger);
        return null;
    }

    public RpcAnnotatedTag toAnnotatedTag() {
        RpcAnnotatedTag.Builder builder = RpcAnnotatedTag.newBuilder()
                .setMessage(nullToEmpty(message))
                .setTag(toTag())
                .setTagger(tagger.toPerson());
        if (taggerTimestamp != null) {
            builder.setTaggerTimestamp(Timestamp.newBuilder()
                    .setNanos(taggerTimestamp.getNano())
                    .setSeconds(taggerTimestamp.getEpochSecond())
                    .build());
        }
        return builder.build();
    }

    @Override
    public String toString() {
        return "AnnotatedTag{id='" + getId() +
               "', latestCommit='" + getLatestCommit() +
               "', hash='" + getHash() +
               "', message='" + abbreviate(message, 50) +
               "', tagger='" + tagger.getName() +
               "', taggerTimestamp='" + taggerTimestamp +
               "'}";
    }

    public static class Builder extends AbstractGitTagBuilder<AnnotatedTag, Builder> {

        private String message;
        private Person tagger;
        private Instant taggerTimestamp;

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

        public Builder tagger(Person tagger) {
            this.tagger = tagger;
            return self();
        }

        public Builder taggerTimestamp(Instant value) {
            taggerTimestamp = value;
            return self();
        }

        @Nonnull
        @Override
        public Builder hash(@Nonnull String hash) {
            return super.hash(requireNonNull(hash, "hash"));
        }

        @Nonnull
        @Override
        public AnnotatedTag build() {
            return new AnnotatedTag(self());
        }

        @Nonnull
        @Override
        protected Builder self() {
            return this;
        }
    }

}
