package main

import (
	"bytes"
	"io"
	"log"
	"net/http"
	"path"
	"strings"

	restful "github.com/emicklei/go-restful"

	"novit.tech/direktil/pkg/localconfig"

	"novit.tech/direktil/local-server/pkg/mime"
)

var sshKeyTypes = []string{"rsa", "ecdsa", "ed25519"}

type AssetVariant struct {
	name string
	url  string
}

func hostAssetVariants(asset string) []AssetVariant {
	switch asset {
	case "uki":
		return []AssetVariant{
			{asset, asset},
			{asset + " (serial)", asset + "?serial=0"},
		}
	case "boot.img",
		"boot.img.gz",
		"boot.qcow2",
		"boot.qed",
		"boot.vdi",
		"boot.vmdk",
		"boot.vpc",
		"boot.iso",
		"boot.tar":
		return []AssetVariant{
			{asset, asset},
			{asset + " (UKI)", asset + "?uki"},
			{asset + " (UKI+serial)", asset + "?uki&serial=0"},
		}
	default:
		return []AssetVariant{{asset, asset}}
	}
}

type wsHost struct {
	hostDoc string
	getHost func(req *restful.Request) (hostName string, err error)
}

func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
	b := func(what string) *restful.RouteBuilder {
		return rws.GET("/" + what).To(ws.render)
	}

	for _, rb := range []*restful.RouteBuilder{
		rws.GET("").To(ws.get).
			Produces(mime.JSON).
			Doc("Get the "+ws.hostDoc+"'s details").
			Returns(200, "OK", localconfig.Host{}),

		// raw configuration
		b("config").
			Produces(mime.YAML).
			Doc("Get the " + ws.hostDoc + "'s configuration"),

		b("config.json").
			Doc("Get the " + ws.hostDoc + "'s configuration (as JSON)"),

		// metal/local HDD install
		b("boot.img").
			Produces(mime.DISK).
			Doc("Get the " + ws.hostDoc + "'s boot disk image"),

		// - raw + compressed
		b("boot.img.gz").
			Produces(mime.DISK + "+gzip").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, gzip compressed"),

		// - other formats
		b("boot.qcow2").
			Produces(mime.DISK + "+qcow2").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, QCOW2 (KVM, Xen)"),
		b("boot.qed").
			Produces(mime.DISK + "+qed").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, QED (KVM)"),
		b("boot.vdi").
			Produces(mime.DISK + "+vdi").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, VDI (VirtualBox)"),
		b("boot.vpc").
			Produces(mime.DISK + "+vpc").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, VHD (Hyper-V)"),
		b("boot.vmdk").
			Produces(mime.DISK + "+vmdk").
			Doc("Get the " + ws.hostDoc + "'s boot disk image, VMDK (VMware)"),

		// metal/local HDD upgrades
		b("boot.tar").
			Produces(mime.TAR).
			Doc("Get the " + ws.hostDoc + "'s /boot archive"),

		// read-only ISO support
		b("boot.iso").
			Produces(mime.ISO).
			Param(cmdlineParam).
			Doc("Get the " + ws.hostDoc + "'s boot CD-ROM image"),

		// netboot support
		b("ipxe").
			Produces(mime.IPXE).
			Doc("Get the " + ws.hostDoc + "'s IPXE code (for netboot)"),

		// boot support
		b("kernel").
			Produces(mime.OCTET).
			Doc("Get the " + ws.hostDoc + "'s kernel (ie: for netboot)"),
		b("initrd").
			Produces(mime.OCTET).
			Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
		b("uki").
			Produces(mime.OCTET).
			Doc("Get the " + ws.hostDoc + "'s Unified Kernel Image"),

		// - bootstrap config
		b("bootstrap-config").
			Produces(mime.YAML).
			Doc("Get the " + ws.hostDoc + "'s bootstrap configuration"),
		b("bootstrap-config.json").
			Doc("Get the " + ws.hostDoc + "'s bootstrap configuration (as JSON)"),
		// - bootstrap
		b("bootstrap.tar").
			Produces(mime.TAR).
			Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),

		// ssh
		b("ssh.pub").Produces("text/plain").Doc("Get the " + ws.hostDoc + "'s ssh public keys"),
		b("known_hosts").Produces("text/plain").Doc("Get the " + ws.hostDoc + "'s ssh known_hosts fragment"),
	} {
		alterRB(rb)
		rws.Route(rb)
	}
}

func (ws wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
	hostname, err := ws.getHost(req)
	if err != nil {
		wsError(resp, err)
		return
	}
	if hostname == "" {
		wsNotFound(resp)
		return
	}

	cfg, err = readConfig()
	if err != nil {
		wsError(resp, err)
		return
	}

	host = hostOrTemplate(cfg, hostname)
	if host == nil {
		wsNotFound(resp)
		return
	}

	return
}

func (ws wsHost) get(req *restful.Request, resp *restful.Response) {
	host, _ := ws.host(req, resp)
	if host == nil {
		return
	}

	resp.WriteEntity(host)
}

func (ws wsHost) render(req *restful.Request, resp *restful.Response) {
	host, cfg := ws.host(req, resp)
	if host == nil {
		return
	}

	what := path.Base(req.Request.URL.Path)

	renderHost(resp.ResponseWriter, req.Request, what, host, cfg)
}

func renderHost(w http.ResponseWriter, r *http.Request, what string, host *localconfig.Host, cfg *localconfig.Config) {
	ctx, err := newRenderContext(host, cfg)
	if err != nil {
		log.Printf("host %s: %s: failed to render: %v", what, host.Name, err)
		http.Error(w, "", http.StatusServiceUnavailable)
		return
	}

	cmdline := r.FormValue("cmdline")

	if s := r.FormValue("serial"); s != "" {
		if cmdline != "" {
			cmdline += " "
		}
		cmdline += "console=tty0 console=ttyS" + s + ",115200"
	}

	_, uki := r.Form["uki"]

	withUki := func(callback func(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error)) func(io.Writer, *renderContext) error {
		return func(out io.Writer, ctx *renderContext) error {
			return callback(out, ctx, uki, cmdline)
		}
	}

	switch what {
	case "kernel":
		err = renderKernel(w, r, ctx)
	case "initrd":
		err = renderCtx(w, r, ctx, what, buildInitrd)
	case "uki":
		uki = true
		err = renderCtx(w, r, ctx, what, withUki(buildUki))

	case "bootstrap.tar":
		err = renderCtx(w, r, ctx, what, buildBootstrap)

	case "boot.img":
		err = renderCtx(w, r, ctx, what, withUki(buildBootImg))
	case "boot.img.gz":
		err = renderCtx(w, r, ctx, what, withUki(buildBootImgGZ))
	case "boot.qcow2":
		err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2", uki, cmdline))
	case "boot.qed":
		err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed", uki, cmdline))
	case "boot.vdi":
		err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi", uki, cmdline))
	case "boot.vmdk":
		err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk", uki, cmdline))
	case "boot.vpc":
		err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc", uki, cmdline))
	case "boot.iso":
		err = renderCtx(w, r, ctx, what, buildBootISO)

	case "boot.tar":
		err = renderCtx(w, r, ctx, what, withUki(buildBootTar))

	// boot v2
	case "bootstrap-config":
		err = renderBootstrapConfig(w, ctx)

	case "config":
		err = renderConfig(w, r, ctx, false)
	case "config.json":
		err = renderConfig(w, r, ctx, true)

	case "ipxe":
		err = renderIPXE(w, ctx)

	case "ssh.pub":
		err = renderSshPub(w, ctx)
	case "known_hosts":
		err = renderKnownHosts(w, ctx)

	default:
		http.NotFound(w, r)
	}

	if err != nil {
		log.Printf("host %s: %s: failed to render: %v", what, host.Name, err)
		http.Error(w, "", http.StatusServiceUnavailable)
	}
}

func renderSshPub(w http.ResponseWriter, ctx *renderContext) (err error) {
	buf := new(bytes.Buffer)

	for _, t := range sshKeyTypes {
		k, err := ctx.sshHostKeyPair(t)
		if err != nil {
			return err
		}
		buf.WriteString(k.Public)
	}

	_, err = w.Write(buf.Bytes())
	return
}

func renderKnownHosts(w http.ResponseWriter, ctx *renderContext) (err error) {
	buf := new(bytes.Buffer)

	names := append([]string{ctx.Host.Name},
		ctx.Host.IPs...)

	for _, name := range names {
		for _, t := range sshKeyTypes {
			k, err := ctx.sshHostKeyPair(t)
			if err != nil {
				return err
			}

			fields := strings.Fields(k.Public)

			buf.WriteString(name)
			buf.WriteByte(' ')
			buf.WriteString(fields[0])
			buf.WriteByte(' ')
			buf.WriteString(fields[1])
			buf.WriteByte('\n')
		}
	}

	_, err = w.Write(buf.Bytes())
	return
}
