package org.itxtech.daedalus.provider;

import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructPollfd;
import android.util.Log;
import androidx.annotation.NonNull;
import okhttp3.OkHttpClient;
import org.itxtech.daedalus.Daedalus;
import org.itxtech.daedalus.service.DaedalusVpnService;
import org.itxtech.daedalus.util.Logger;
import org.itxtech.daedalus.server.DnsServerHelper;
import org.minidns.dnsmessage.DnsMessage;
import org.pcap4j.packet.IpPacket;
import org.pcap4j.packet.IpSelector;
import org.pcap4j.packet.UdpPacket;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/**
 * Daedalus Project
 *
 * @author iTX Technologies
 * @link https://itxtech.org
 * <p>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 */
abstract public class HttpsProvider extends Provider {

    public static final String HTTPS_SUFFIX = "https://";

    private static final String TAG = "HttpsProvider";

    final WhqList whqList = new WhqList();

    HttpsProvider(ParcelFileDescriptor descriptor, DaedalusVpnService service) {
        super(descriptor, service);
    }

    OkHttpClient getHttpClient(String accept) {
        return new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .addInterceptor((chain) -> chain.proceed(chain.request().newBuilder()
                        .header("Accept", accept)
                        .build()))
                .dns(hostname -> {
                    if (DnsServerHelper.domainCache.containsKey(hostname)) {
                        return DnsServerHelper.domainCache.get(hostname);
                    }
                    return Arrays.asList(InetAddress.getAllByName(hostname));
                })
                .build();
    }

    public void process() {
        try {
            FileDescriptor[] pipes = Os.pipe();
            mInterruptFd = pipes[0];
            mBlockFd = pipes[1];
            FileInputStream inputStream = new FileInputStream(descriptor.getFileDescriptor());
            FileOutputStream outputStream = new FileOutputStream(descriptor.getFileDescriptor());

            byte[] packet = new byte[32767];
            while (running) {
                StructPollfd deviceFd = new StructPollfd();
                deviceFd.fd = inputStream.getFD();
                deviceFd.events = (short) OsConstants.POLLIN;
                StructPollfd blockFd = new StructPollfd();
                blockFd.fd = mBlockFd;
                blockFd.events = (short) (OsConstants.POLLHUP | OsConstants.POLLERR);

                if (!deviceWrites.isEmpty())
                    deviceFd.events |= (short) OsConstants.POLLOUT;

                StructPollfd[] polls = new StructPollfd[2];
                polls[0] = deviceFd;
                polls[1] = blockFd;
                Os.poll(polls, 100);
                if (blockFd.revents != 0) {
                    Log.i(TAG, "Told to stop VPN");
                    running = false;
                    return;
                }

                Iterator<WaitingHttpsRequest> iterator = whqList.iterator();
                while (iterator.hasNext()) {
                    WaitingHttpsRequest request = iterator.next();
                    if (request.completed) {
                        handleDnsResponse(request.packet, request.result);
                        iterator.remove();
                    }
                }

                if ((deviceFd.revents & OsConstants.POLLOUT) != 0) {
                    Log.d(TAG, "Write to device");
                    writeToDevice(outputStream);
                }
                if ((deviceFd.revents & OsConstants.POLLIN) != 0) {
                    Log.d(TAG, "Read from device");
                    readPacketFromDevice(inputStream, packet);
                }
                service.providerLoopCallback();
            }
        } catch (Exception e) {
            Logger.logException(e);
        }
    }

    @Override
    protected void handleDnsRequest(byte[] packetData) {
        IpPacket parsedPacket;
        try {
            parsedPacket = (IpPacket) IpSelector.newPacket(packetData, 0, packetData.length);
        } catch (Exception e) {
            return;
        }

        if (!(parsedPacket.getPayload() instanceof UdpPacket)) {
            return;
        }

        InetAddress destAddr = parsedPacket.getHeader().getDstAddr();
        if (destAddr == null)
            return;
        String uri;
        try {
            uri = service.dnsServers.get(destAddr.getHostAddress()).getAddress();//https uri
        } catch (Exception e) {
            Logger.logException(e);
            return;
        }

        UdpPacket parsedUdp = (UdpPacket) parsedPacket.getPayload();

        if (parsedUdp.getPayload() == null) {
            return;
        }

        byte[] dnsRawData = (parsedUdp).getPayload().getRawData();
        DnsMessage dnsMsg;
        try {
            dnsMsg = new DnsMessage(dnsRawData);
            if (Daedalus.getPrefs().getBoolean("settings_debug_output", false)) {
                Logger.debug("DnsRequest: " + dnsMsg.toString());
            }
        } catch (IOException e) {
            return;
        }
        if (dnsMsg.getQuestion() == null) {
            Logger.debug("handleDnsRequest: Discarding DNS packet with no query " + dnsMsg);
            return;
        }

        if (!resolve(parsedPacket, dnsMsg) && uri != null) {
            sendRequestToServer(parsedPacket, dnsMsg, uri);
            //SHOULD use a DNS ID of 0 in every DNS request (according to draft-ietf-doh-dns-over-https-11)
        }
    }

    protected abstract void sendRequestToServer(IpPacket parsedPacket, DnsMessage message, String uri);
    //uri example: 1.1.1.1:1234/dnsQuery. The specified provider will add https:// and parameters

    public abstract static class WaitingHttpsRequest {
        public boolean completed = false;
        public byte[] result;
        public final IpPacket packet;

        public WaitingHttpsRequest(IpPacket packet) {
            this.packet = packet;
        }

        public abstract void doRequest();
    }

    public static class WhqList implements Iterable<WaitingHttpsRequest> {
        private final LinkedList<WaitingHttpsRequest> list = new LinkedList<>();

        public void add(WaitingHttpsRequest request) {
            list.add(request);
            request.doRequest();
        }

        @NonNull
        public Iterator<WaitingHttpsRequest> iterator() {
            return list.iterator();
        }
    }
}