package wtf.sten.vault;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;

public final class Proxy extends ChannelDuplexHandler {
	private final InetSocketAddress proxy;
	private final String user;
	private final String pass;
	private InetSocketAddress dest;
	private ChannelPromise connectPromise;
	private int step;
	private ByteBuf pending;

	private Proxy(InetSocketAddress proxy, String user, String pass) {
		this.proxy = proxy;
		this.user = user == null ? "" : user;
		this.pass = pass == null ? "" : pass;
	}

	public static void install(Channel channel) {
		Parsed parsed = parse(VaultConfig.get().proxy);
		if (parsed == null) return;
		channel.pipeline().addFirst("sten-socks", new Proxy(parsed.address, parsed.user, parsed.pass));
	}

	public static Parsed parse(String raw) {
		if (raw == null) return null;
		String value = raw.trim();
		if (value.isEmpty()) return null;
		if (value.startsWith("socks5://") || value.startsWith("socks://")) {
			value = value.substring(value.indexOf("://") + 3);
		}
		String user = "";
		String pass = "";
		int at = value.lastIndexOf('@');
		if (at > 0) {
			String auth = value.substring(0, at);
			value = value.substring(at + 1);
			int colon = auth.indexOf(':');
			if (colon >= 0) {
				user = auth.substring(0, colon);
				pass = auth.substring(colon + 1);
			} else {
				user = auth;
			}
		}
		int colon = value.lastIndexOf(':');
		if (colon <= 0 || colon == value.length() - 1) return null;
		String host = value.substring(0, colon).replace("[", "").replace("]", "");
		int port;
		try {
			port = Integer.parseInt(value.substring(colon + 1));
		} catch (Exception ignored) {
			return null;
		}
		if (host.isBlank() || port <= 0 || port > 65535) return null;
		return new Parsed(new InetSocketAddress(host, port), user, pass);
	}

	@Override
	public void connect(ChannelHandlerContext ctx, SocketAddress remote, SocketAddress local, ChannelPromise promise) {
		if (!(remote instanceof InetSocketAddress address)) {
			ctx.connect(remote, local, promise);
			return;
		}
		dest = address;
		connectPromise = promise;
		ctx.connect(proxy, local, ctx.newPromise().addListener(future -> {
			if (!future.isSuccess() && connectPromise != null) {
				connectPromise.setFailure(future.cause());
			}
		}));
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		super.channelActive(ctx);
		ByteBuf hello = ctx.alloc().buffer(4);
		hello.writeByte(5);
		if (user.isEmpty()) {
			hello.writeByte(1);
			hello.writeByte(0);
		} else {
			hello.writeByte(2);
			hello.writeByte(0);
			hello.writeByte(2);
		}
		ctx.writeAndFlush(hello);
		step = 1;
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		if (step >= 4 || !(msg instanceof ByteBuf buf)) {
			ctx.fireChannelRead(msg);
			return;
		}
		if (pending == null) pending = ctx.alloc().buffer();
		pending.writeBytes(buf);
		buf.release();
		while (consume(ctx)) {
		}
	}

	private boolean consume(ChannelHandlerContext ctx) {
		if (step == 1) {
			if (pending.readableBytes() < 2) return false;
			pending.readByte();
			int method = pending.readByte() & 0xff;
			if (method == 2 && !user.isEmpty()) {
				byte[] u = user.getBytes(StandardCharsets.UTF_8);
				byte[] p = pass.getBytes(StandardCharsets.UTF_8);
				ByteBuf auth = ctx.alloc().buffer(3 + u.length + p.length);
				auth.writeByte(1);
				auth.writeByte(u.length);
				auth.writeBytes(u);
				auth.writeByte(p.length);
				auth.writeBytes(p);
				ctx.writeAndFlush(auth);
				step = 2;
			} else if (method == 0) {
				sendConnect(ctx);
				step = 3;
			} else {
				fail(ctx, "SOCKS5 method rejected");
			}
			return true;
		}
		if (step == 2) {
			if (pending.readableBytes() < 2) return false;
			pending.readByte();
			if ((pending.readByte() & 0xff) != 0) {
				fail(ctx, "SOCKS5 login rejected");
				return false;
			}
			sendConnect(ctx);
			step = 3;
			return true;
		}
		if (step == 3) {
			if (pending.readableBytes() < 4) return false;
			pending.markReaderIndex();
			pending.readByte();
			int status = pending.readByte() & 0xff;
			pending.readByte();
			int atyp = pending.readByte() & 0xff;
			int extra = atyp == 1 ? 6 : atyp == 4 ? 18 : atyp == 3 ? 1 : -1;
			if (extra < 0) {
				fail(ctx, "SOCKS5 bind failed");
				return false;
			}
			if (atyp == 3) {
				if (pending.readableBytes() < 1) {
					pending.resetReaderIndex();
					return false;
				}
				extra = (pending.readByte() & 0xff) + 2;
			}
			if (pending.readableBytes() < extra) {
				pending.resetReaderIndex();
				return false;
			}
			pending.skipBytes(extra);
			if (status != 0) {
				fail(ctx, "SOCKS5 connect failed (" + status + ")");
				return false;
			}
			step = 4;
			if (connectPromise != null) connectPromise.trySuccess();
			if (pending.readableBytes() > 0) ctx.fireChannelRead(pending.retain());
			pending.release();
			pending = null;
			return false;
		}
		return false;
	}

	private void sendConnect(ChannelHandlerContext ctx) {
		String host = dest.getHostString();
		byte[] name = host.getBytes(StandardCharsets.US_ASCII);
		ByteBuf req = ctx.alloc().buffer(7 + name.length);
		req.writeByte(5);
		req.writeByte(1);
		req.writeByte(0);
		req.writeByte(3);
		req.writeByte(name.length);
		req.writeBytes(name);
		req.writeShort(dest.getPort());
		ctx.writeAndFlush(req);
	}

	private void fail(ChannelHandlerContext ctx, String reason) {
		if (connectPromise != null) connectPromise.tryFailure(new IllegalStateException(reason));
		ctx.close();
	}

	public record Parsed(InetSocketAddress address, String user, String pass) {
	}
}
