package leigh.esp.kabe;

import jakarta.servlet.ServletException;
import leigh.BuildVersion;
import leigh.esp.kabe.http.PrivateHttpBus;
import leigh.esp.kabe.http.PublicHttpBus;
import leigh.esp.keikai.service.KeikaiService;
import leigh.mecha.http.server.WebTransaction;
import leigh.mecha.lang.DangerousRunnable;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.util.NumberUtil;
import leigh.mecha.util.RuntimeMonitor;
import leigh.mecha.util.UniversalJob;
import nl.basjes.collections.prefixmap.ASCIIPrefixMap;
import org.apache.http.config.SocketConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Properties;

/**
 * KabeServer is the edge HTTP server for the Enhanced Services Platform (ESP). Kabe was previously simply known as
 * "Engine" in ESP platform versions prior to 16.
 *
 * @author Alex Leigh
 */
public class KabeServer implements DangerousRunnable {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(KabeServer.class);
    private final ArrayList<ServiceBus> serviceBuses = new ArrayList<>();
    private final ASCIIPrefixMap<Service> services = new ASCIIPrefixMap<>(false);
    private final CloseableHttpClient egressClient;
    private final Properties properties;
    private final PoolingHttpClientConnectionManager cm;
    private final byte[] fourOhFourPage = ("404 Not Found\n" + BuildVersion.ESP_PROD_STR).getBytes(StandardCharsets.UTF_8);

    public KabeServer(Properties properties) {
        this.properties = properties;
        cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(10000);
        cm.setDefaultMaxPerRoute(1000);

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    PoolStats ps = cm.getTotalStats();
                    logger.info("HTTP Client Stats: {}", ps);
                    try {
                        Thread.sleep(10 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        SocketConfig socketConfig = SocketConfig.custom()
                .setSoKeepAlive(true)
                .setTcpNoDelay(true)
                .build();
        egressClient = HttpClientBuilder.create().setConnectionManager(cm).setDefaultSocketConfig(socketConfig).build();
        serviceBuses.add(new PublicHttpBus(this, 8123));
        serviceBuses.add(new PrivateHttpBus(this, 8124));
    }

    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();

        if (args.length >= 1) {
            properties.load(new BufferedInputStream(new FileInputStream(args[0])));
        }

        KabeServer kabe = new KabeServer(properties);
        Thread t = new Thread(kabe);
        t.join();
        t.start();
    }

    @Override
    public void runDangerously() throws Exception {
        logger.info("\n         _           _            _      \n" +
                "        /\\ \\        / /\\         /\\ \\    \n" +
                "       /  \\ \\      / /  \\       /  \\ \\   \n" +
                "      / /\\ \\ \\    / / /\\ \\__   / /\\ \\ \\  \n" +
                "     / / /\\ \\_\\  / / /\\ \\___\\ / / /\\ \\_\\ \n" +
                "    / /_/_ \\/_/  \\ \\ \\ \\/___// / /_/ / / \n" +
                "   / /____/\\      \\ \\ \\     / / /__\\/ /  \n" +
                "  / /\\____\\/  _    \\ \\ \\   / / /_____/   \n" +
                " / / /______ /_/\\__/ / /  / / /          \n" +
                "/ / /_______\\\\ \\/___/ /  / / /           \n" +
                "\\/__________/ \\_____\\/   \\/_/            \n" +
                "                                   ");

        UniversalJob.banner(logger, BuildVersion.ESP_PROD_STR);

        logger.info("Hardware Configuration: [CPU cores: {}] [RAM max: {}]",
                Runtime.getRuntime().availableProcessors(),
                NumberUtil.humanByteSize(Runtime.getRuntime().maxMemory()));

        Thread t = new Thread(new RuntimeMonitor());
        t.start();

        updateService(new KeikaiService(this));

        for (ServiceBus sb : serviceBuses) {
            sb.start();
        }
    }

    public void updateService(Service service) {
        Service oldSvc = services.get(service.getPath());
        if (oldSvc == null) {
            logger.info("Updating service. [svc: {}]", service);
        }
        services.put(service.getPath(), service);
    }

    public Service getService(String path) {
        Service s = services.getLongestMatch(path);

        // TODO Evaluating for eviction on every request is stupid.
        if (s != null) {
            Duration age = Duration.between(s.lastKnownGood(), Instant.now());
            if (age.getSeconds() > 10) {
                services.remove(s.getPath());
                return null;
            }
        }
        return s;
    }

    public void handle(WebTransaction request) throws IOException, ClassNotFoundException, ServletException {
        Service svc = getService(request.httpServletRequest.getRequestURI());
        if (svc == null) {
            request.httpServletResponse.setStatus(404);
            request.httpServletResponse.setContentType("text/plain");
            request.httpServletResponse.getOutputStream().write(fourOhFourPage);
            return;
        }
        svc.handleClientRequest(request);
    }

    public CloseableHttpClient getEgressClient() {
        return egressClient;
    }

    @Override
    public void run() {
        try {
            runDangerously();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Properties getProperties() {
        return properties;
    }
}
