package quant.rich.emoney.patch.okhttp;

import java.util.*;
import java.util.function.*;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import quant.rich.emoney.patch.okhttp.PatchOkHttp.HeaderInterceptor;


public class PatchOkHttpRule {
    
    @Getter
    @Setter
    @Accessors(chain=true)
    private Integer id;
    private final Predicate<RequestContext> condition;
    private final List<HeaderAction> actions;

    public PatchOkHttpRule(Predicate<RequestContext> condition, List<HeaderAction> actions) {
        this.condition = condition;
        this.actions = actions;
    }

    public boolean matches(RequestContext ctx) {
        return condition.test(ctx);
    }

    public void apply(RequestContext ctx, String currentHeader, Consumer<String> overrideSetter) {
        for (HeaderAction action : actions) {
            action.apply(ctx, currentHeader, overrideSetter);
        }
    }

    @FunctionalInterface
    public interface HeaderAction {
        void apply(RequestContext ctx, String currentHeader, Consumer<String> overrideValueSetter);
    }
    
    /**
     * 起始语句，创建一个 Builder
     * @return
     */
    public static Builder when() {
        return new Builder(ctx -> true);
    }

    public static class Builder {
        public static final Logger log = LoggerFactory.getLogger(HeaderInterceptor.class);
        private Predicate<RequestContext> condition;
        private final List<HeaderAction> actions = new ArrayList<>();

        public Builder(Predicate<RequestContext> initial) {
            this.condition = initial;
        }

        public Builder hasHeaderPair(String name, String value) {
            return and(ctx -> value.equals(ctx.headers.get(name)));
        }

        public Builder hasHeaderName(String name) {
            return and(ctx -> ctx.headers.containsKey(name));
        }
        
        public Builder hasNotHeaderName(String name) {
            return not(r -> r.and(ctx -> ctx.headers.containsKey(name)));
        }

        public Builder hasHeaderValueMatch(String name, String regex) {
            Pattern pattern = Pattern.compile(regex);
            return and(ctx -> {
                String val = ctx.headers.get(name);
                return val != null && pattern.matcher(val).matches();
            });
        }

        public Builder hostEndsWith(String suffix) {
            return and(ctx -> ctx.host != null && ctx.host.endsWith(suffix));
        }

        public Builder hostContains(String keyword) {
            return and(ctx -> ctx.host != null && ctx.host.contains(keyword));
        }
        
        /**
         * 
         * @param pattern
         * @return
         */
        public Builder hostMatches(String pattern) {
            return and(ctx -> ctx.host != null && Pattern.matches(pattern, ctx.host));
        }

        public Builder isHttp() {
            return and(ctx -> "http".equalsIgnoreCase(ctx.scheme));
        }

        public Builder isHttps() {
            return and(ctx -> "https".equalsIgnoreCase(ctx.scheme));
        }
        
        /**
         * 接收一个 not 条件<br>
         * 例：Rule.when().not(r -> r.isHttps()).build()
         * 
         * @param block
         * @return
         */
        public Builder not(Consumer<Builder> block) {
            Builder sub = new Builder(ctx -> true);      // starts with true
            block.accept(sub);
            this.condition = this.condition
                             .and(sub.condition.negate());
            return this;
        }

        public Builder or(Consumer<Builder> block) {
            Builder sub = new Builder(ctx -> true);
            block.accept(sub);
            this.condition = this.condition.or(sub.condition);
            return this;
        }

        public Builder or(Builder other) {
            this.condition = this.condition.or(other.condition);
            return this;
        }

        public Builder or(PatchOkHttpRule rule) {
            this.condition = this.condition.or(rule::matches);
            return this;
        }

        public Builder and() {
            return this;
        }

        public Builder and(Predicate<RequestContext> other) {
            this.condition = this.condition.and(other);
            return this;
        }

        public Builder then() {
            return this;
        }

        public Builder setHeader(String name, String value) {
            actions.add((ctx, curr, setter) -> ctx.headers.put(name, value));
            return this;
        }

        public Builder overrideHeader(String headerName, String value) {
            actions.add((ctx, curr, setter) -> {
                if (curr.equalsIgnoreCase(headerName)) {
                    log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName,
                            value, this.getClass().getClassLoader());
                    setter.accept(value);
                }
            });
            return this;
        }
        
        /**
         * 如果满足条件则覆写指定 Header。当覆写值可能动态变化时，使用本方法提供 supplier
         * @param headerName
         * @param valueSupplier
         * @return
         */
        public Builder overrideHeader(String headerName, Supplier<String> valueSupplier) {
            actions.add((ctx, curr, setter) -> {
                if (curr.equalsIgnoreCase(headerName)) {
                    String value = valueSupplier.get();
                    log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName,
                            value, this.getClass().getClassLoader());
                    setter.accept(value);
                }
            });
            return this;
        }

        public PatchOkHttpRule build() {
            return new PatchOkHttpRule(condition, actions);
        }
    }
    
    public boolean equals(PatchOkHttpRule other) {
        if (other == null) return false;
        if (this.id == null || other.id == null) return false;
        return this.id.equals(other.id);
    }
    
    public int hashCode() {
        return Objects.hash(id);
    }
    
}