package org.pdown.core.util;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.resolver.NoopAddressResolverGroup;
import io.netty.util.internal.StringUtil;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLException;
import org.pdown.core.boot.HttpDownBootstrap;
import org.pdown.core.entity.HttpHeadsInfo;
import org.pdown.core.entity.HttpRequestInfo;
import org.pdown.core.entity.HttpRequestInfo.HttpVer;
import org.pdown.core.entity.HttpResponseInfo;
import org.pdown.core.exception.BootstrapResolveException;
import org.pdown.core.proxy.ProxyConfig;
import org.pdown.core.proxy.ProxyHandleFactory;
import org.pdown.core.util.ProtoUtil.RequestProto;

public class HttpDownUtil {

  private static SslContext sslContext;

  public static SslContext getSslContext() throws SSLException {
    if (sslContext == null) {
      synchronized (HttpDownUtil.class) {
        if (sslContext == null) {
          sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        }
      }
    }
    return sslContext;
  }

  /**
   * 检测响应相关信息
   */
  public static HttpResponseInfo getHttpResponseInfo(HttpRequest httpRequest, HttpHeaders resHeaders, ProxyConfig proxyConfig, NioEventLoopGroup loopGroup)
      throws Exception {
    HttpResponse httpResponse = null;
    if (resHeaders == null) {
      httpResponse = getResponse(httpRequest, proxyConfig, loopGroup);
      //处理重定向
      if ((httpResponse.status().code() + "").indexOf("30") == 0) {
        //TODO 302重定向乱码 https://link.gimhoy.com/googledrive/aHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL29wZW4/aWQ9MThlVmNKeEhwaE40RUpGTUowSk10bWNXOVhCcWJhVE1k.jpg
        String redirectUrl = httpResponse.headers().get(HttpHeaderNames.LOCATION);
        HttpRequestInfo requestInfo = (HttpRequestInfo) httpRequest;
        //重定向cookie设置
        List<String> setCookies = httpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE);
        if (setCookies != null && setCookies.size() > 0) {
          StringBuilder requestCookie = new StringBuilder();
          String oldRequestCookie = requestInfo.headers().get(HttpHeaderNames.COOKIE);
          if (oldRequestCookie != null) {
            requestCookie.append(oldRequestCookie);
          }
          String split = String.valueOf((char) HttpConstants.SEMICOLON) + String.valueOf(HttpConstants.SP_CHAR);
          for (String setCookie : setCookies) {
            String cookieNV = setCookie.split(split)[0];
            if (requestCookie.length() > 0) {
              requestCookie.append(split);
            }
            requestCookie.append(cookieNV);
          }
          requestInfo.headers().set(HttpHeaderNames.COOKIE, requestCookie.toString());
        }
        requestInfo.headers().remove(HttpHeaderNames.HOST);
        requestInfo.setUri(redirectUrl);
        RequestProto requestProto = ProtoUtil.getRequestProto(requestInfo);
        requestInfo.headers().set(HttpHeaderNames.HOST, requestProto.getHost());
        requestInfo.setRequestProto(requestProto);
        return getHttpResponseInfo(httpRequest, null, proxyConfig, loopGroup);
      }
    }
    if (httpResponse == null) {
      httpResponse = getResponse(httpRequest, proxyConfig, loopGroup);
    }
    if (httpResponse.status().code() != HttpResponseStatus.OK.code()
        && httpResponse.status().code() != HttpResponseStatus.PARTIAL_CONTENT.code()) {
      throw new BootstrapResolveException("Status code exception:" + httpResponse.status().code());
    }
    return parseResponse(httpRequest, httpResponse);
  }

  public static String getDownFileName(HttpRequest httpRequest, HttpHeaders resHeaders) {
    String fileName = null;
    String disposition = resHeaders.get(HttpHeaderNames.CONTENT_DISPOSITION);
    if (disposition != null) {
      //attachment;filename=1.rar   attachment;filename=*UTF-8''1.rar
      Pattern pattern = Pattern.compile("^.*filename\\*?=\"?(?:.*'')?([^\"]*)\"?$");
      Matcher matcher = pattern.matcher(disposition);
      if (matcher.find()) {
        char[] chs = matcher.group(1).toCharArray();
        byte[] bts = new byte[chs.length];
        //netty将byte转成了char,导致中文乱码 HttpObjectDecoder(:803)
        for (int i = 0; i < chs.length; i++) {
          bts[i] = (byte) chs[i];
        }
        try {
          fileName = new String(bts, "UTF-8");
          fileName = URLDecoder.decode(fileName, "UTF-8");
        } catch (Exception e) {

        }
      }
    }
    if (fileName == null) {
      Pattern pattern = Pattern.compile("^.*/([^/?]*\\.[^./?]+)(\\?[^?]*)?$");
      Matcher matcher = pattern.matcher(httpRequest.uri());
      if (matcher.find()) {
        fileName = matcher.group(1);
      } else {
        fileName = "Unknown";
      }
    }
    return fileName;
  }

  /**
   * 取当前请求的ContentLength
   */
  public static long getDownContentSize(HttpHeaders resHeaders) {
    String contentRange = resHeaders.get(HttpHeaderNames.CONTENT_RANGE);
    if (contentRange != null) {
      Pattern pattern = Pattern.compile("^[^\\d]*(\\d+)-(\\d+)/.*$");
      Matcher matcher = pattern.matcher(contentRange);
      if (matcher.find()) {
        long startSize = Long.parseLong(matcher.group(1));
        long endSize = Long.parseLong(matcher.group(2));
        return endSize - startSize + 1;
      }
    } else {
      String contentLength = resHeaders.get(HttpHeaderNames.CONTENT_LENGTH);
      if (contentLength != null) {
        return Long.valueOf(resHeaders.get(HttpHeaderNames.CONTENT_LENGTH));
      }
    }
    return 0;
  }

  /**
   * 取请求下载文件的总大小
   */
  public static long getDownFileSize(HttpHeaders resHeaders) {
    String contentRange = resHeaders.get(HttpHeaderNames.CONTENT_RANGE);
    if (contentRange != null) {
      Pattern pattern = Pattern.compile("^.*/(\\d+).*$");
      Matcher matcher = pattern.matcher(contentRange);
      if (matcher.find()) {
        return Long.parseLong(matcher.group(1));
      }
    } else {
      String contentLength = resHeaders.get(HttpHeaderNames.CONTENT_LENGTH);
      if (contentLength != null) {
        return Long.valueOf(resHeaders.get(HttpHeaderNames.CONTENT_LENGTH));
      }
    }
    return 0;
  }

  public static HttpResponseInfo parseResponse(HttpRequest httpRequest, HttpResponse httpResponse) {
    HttpResponseInfo responseInfo = new HttpResponseInfo();
    responseInfo.setFileName(getDownFileName(httpRequest, httpResponse.headers()));
    responseInfo.setTotalSize(getDownFileSize(httpResponse.headers()));
    //chunked编码不支持断点下载
    if (httpResponse.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) {
      //206表示支持断点下载
      if (httpResponse.status().equals(HttpResponseStatus.PARTIAL_CONTENT)) {
        responseInfo.setSupportRange(true);
      }
    }
    return responseInfo;
  }

  /**
   * 取请求响应
   */
  public static HttpResponse getResponse(HttpRequest httpRequest, ProxyConfig proxyConfig, NioEventLoopGroup loopGroup) throws Exception {
    final HttpResponse[] httpResponses = new HttpResponse[1];
    CountDownLatch cdl = new CountDownLatch(1);
    HttpRequestInfo requestInfo = (HttpRequestInfo) httpRequest;
    RequestProto requestProto = requestInfo.requestProto();
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(loopGroup) // 注册线程池
        .channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
        .handler(new ChannelInitializer() {

          @Override
          protected void initChannel(Channel ch) throws Exception {
            if (proxyConfig != null) {
              ch.pipeline().addLast(ProxyHandleFactory.build(proxyConfig));
            }
            if (requestProto.getSsl()) {
              ch.pipeline().addLast(getSslContext().newHandler(ch.alloc(), requestProto.getHost(), requestProto.getPort()));
            }
            ch.pipeline().addLast("httpCodec", new HttpClientCodec());
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {

              @Override
              public void channelRead(ChannelHandlerContext ctx0, Object msg0)
                  throws Exception {
                if (msg0 instanceof HttpResponse) {
                  HttpResponse httpResponse = (HttpResponse) msg0;
                  httpResponses[0] = httpResponse;
                  ctx0.channel().close();
                  cdl.countDown();
                }
              }


            });
          }

        });
    if (proxyConfig != null) {
      //代理服务器解析DNS和连接
      bootstrap.resolver(NoopAddressResolverGroup.INSTANCE);
    }
    ChannelFuture cf = bootstrap.connect(requestProto.getHost(), requestProto.getPort());
    cf.addListener((ChannelFutureListener) future -> {
      if (future.isSuccess()) {
        //请求下载一个字节测试是否支持断点下载
        httpRequest.headers().set(HttpHeaderNames.RANGE, "bytes=0-0");
        cf.channel().writeAndFlush(httpRequest);
        if (requestInfo.content() != null) {
          //请求体写入
          HttpContent content = new DefaultLastHttpContent();
          content.content().writeBytes(requestInfo.content());
          cf.channel().writeAndFlush(content);
        }
      } else {
        cdl.countDown();
      }
    });
    cdl.await(60, TimeUnit.SECONDS);
    if (httpResponses[0] == null) {
      throw new TimeoutException("getResponse timeout");
    }
    httpRequest.headers().remove(HttpHeaderNames.RANGE);
    return httpResponses[0];
  }

  /**
   * 关闭tcp连接和文件描述符
   */
  public static void safeClose(Channel channel, Closeable... fileChannels) throws IOException {
    if (channel != null && channel.isOpen()) {
      //关闭旧的下载连接
      channel.close();
    }
    if (fileChannels != null && fileChannels.length > 0) {
      for (Closeable closeable : fileChannels) {
        if (closeable != null) {
          //关闭旧的下载文件连接
          closeable.close();
        }
      }
    }
  }

  public static RequestProto parseRequestProto(String url) throws MalformedURLException {
    return parseRequestProto(new URL(url));
  }

  public static RequestProto parseRequestProto(URL url) {
    int port = url.getPort() == -1 ? url.getDefaultPort() : url.getPort();
    return new RequestProto(url.getHost(), port, url.getProtocol().equalsIgnoreCase("https"));
  }

  public static HttpRequestInfo buildRequest(String method, String url, Map<String, String> heads, String body)
      throws MalformedURLException {
    URL u = new URL(url);
    HttpHeadsInfo headsInfo = new HttpHeadsInfo();
    headsInfo.add("Host", u.getHost());
    headsInfo.add("Connection", "keep-alive");
    headsInfo.add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36");
    headsInfo.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
    headsInfo.add("Referer", u.getHost());
    if (heads != null) {
      for (Entry<String, String> entry : heads.entrySet()) {
        headsInfo.set(entry.getKey(), entry.getValue() == null ? "" : entry.getValue());
      }
    }
    byte[] content = null;
    if (body != null && body.length() > 0) {
      content = body.getBytes();
      headsInfo.add("Content-Length", content.length);
    }
    HttpMethod httpMethod = StringUtil.isNullOrEmpty(method) ? HttpMethod.GET : HttpMethod.valueOf(method.toUpperCase());
    HttpRequestInfo requestInfo = new HttpRequestInfo(HttpVer.HTTP_1_1, httpMethod, u.getFile(), headsInfo, content);
    requestInfo.setRequestProto(parseRequestProto(u));
    return requestInfo;
  }

  public static HttpRequestInfo buildRequest(String url, Map<String, String> heads, String body)
      throws MalformedURLException {
    return buildRequest(null, url, heads, body);
  }

  public static HttpRequestInfo buildRequest(String url, Map<String, String> heads)
      throws MalformedURLException {
    return buildRequest(url, heads, null);
  }

  public static HttpRequestInfo buildRequest(String url)
      throws MalformedURLException {
    return buildRequest(url, null);
  }

  public static String getUrl(HttpRequest request) {
    String host = request.headers().get(HttpHeaderNames.HOST);
    String url;
    if (request.uri().indexOf("/") == 0) {
      if (request.uri().length() > 1) {
        url = host + request.uri();
      } else {
        url = host;
      }
    } else {
      url = request.uri();
    }
    return url;
  }

  /**
   * 取下载文件绝对路径
   */
  public static String getTaskFilePath(HttpDownBootstrap httpDownBootstrap) {
    return httpDownBootstrap.getDownConfig().getFilePath() + File.separator + httpDownBootstrap.getResponse().getFileName();
  }
}