package ssh2

import (
	"fmt"
	"io"
	"net"

	"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
	"github.com/gliderlabs/ssh"
	gossh "golang.org/x/crypto/ssh"
)

// direct-tcpip data struct as specified in RFC4254, Section 7.2
type localForwardChannelData struct {
	DestAddr string
	DestPort uint32

	OriginAddr string
	OriginPort uint32
}

// setupForwardHandler sets up the forwarding handler for the ssh server
func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) {
	if server.ChannelHandlers == nil {
		server.ChannelHandlers = make(map[string]ssh.ChannelHandler)
		for n, h := range ssh.DefaultChannelHandlers {
			server.ChannelHandlers[n] = h
		}
	}
	server.ChannelHandlers["direct-tcpip"] = ssh2.handleDirectTCP
}

type Intercept struct {
	Description string
	Match       component.HostPort
	Dest        component.HostPort
}

// ExamplePort returns a local port that can be forwarded to without root rights
func (i Intercept) ExamplePort() uint32 {
	if i.Match.Port < 100 {
		return i.Match.Port * 101
	}
	if i.Match.Port < 1024 {
		return i.Match.Port * 1001
	}
	return i.Match.Port
}
func (i Intercept) Intercept(req component.HostPort) (intercepted bool, ok bool, dest component.HostPort, rejectReason string) {
	if req.Host != i.Match.Host {
		return false, ok, dest, rejectReason
	}

	if req.Port != i.Match.Port {
		return true, false, dest, fmt.Sprintf("%s listens on port %d", i.Description, i.Match.Port)
	}
	return true, true, i.Dest, ""
}

func (ssh2 *SSH2) Intercepts() []Intercept {
	return ssh2.interceptsC.Get(func() []Intercept {
		return []Intercept{
			{Description: "Triplestore", Match: component.HostPort{Host: "triplestore", Port: 7200}, Dest: ssh2.Upstream.Triplestore},
			{Description: "SQL", Match: component.HostPort{Host: "sql", Port: 3306}, Dest: ssh2.Upstream.SQL},
			{Description: "PHPMyAdmin", Match: component.HostPort{Host: "phpmyadmin", Port: 80}, Dest: component.HostPort{Host: "phpmyadmin", Port: 80}},
		}
	})
}

func (ssh2 *SSH2) getForwardDest(req component.HostPort, ctx ssh.Context) (ok bool, dest component.HostPort, rejectReason string) {
	// check all the intercepts first
	for _, i := range ssh2.Intercepts() {
		intercepted, ok, dest, rejectReason := i.Intercept(req)
		if !intercepted {
			continue
		}
		return ok, dest, rejectReason
	}

	// then check the instances
	slug, ok := ssh2.Config.HTTP.SlugFromHost(req.Host)
	if !ok || req.Port != 22 || !hasPermission(ctx, slug) {
		return false, dest, "permission denied"
	}

	return true, component.HostPort{Host: slug + "." + ssh2.Config.HTTP.PrimaryDomain + ".wisski", Port: 22}, ""
}

// handleDirectTCP handles a direct tcp connection for the server
func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
	d := localForwardChannelData{}
	if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
		newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
		return
	}

	ok, dest, rejectReason := ssh2.getForwardDest(component.HostPort{Host: d.DestAddr, Port: d.DestPort}, ctx)
	if !ok {
		newChan.Reject(gossh.Prohibited, rejectReason)
		return
	}

	var dialer net.Dialer
	dconn, err := dialer.DialContext(ctx, "tcp", dest.String())
	if err != nil {
		newChan.Reject(gossh.ConnectionFailed, err.Error())
		return
	}

	ch, reqs, err := newChan.Accept()
	if err != nil {
		dconn.Close()
		return
	}
	go gossh.DiscardRequests(reqs)

	go func() {
		defer ch.Close()
		defer dconn.Close()
		io.Copy(ch, dconn)
	}()
	go func() {
		defer ch.Close()
		defer dconn.Close()
		io.Copy(dconn, ch)
	}()
}
