package client

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"strconv"
	"sync"

	"github.com/davecgh/go-spew/spew"
	"github.com/rs/zerolog/log"

	"code.dumpstack.io/tools/out-of-tree/api"
	"code.dumpstack.io/tools/out-of-tree/config/dotfiles"
	"code.dumpstack.io/tools/out-of-tree/distro"
	"code.dumpstack.io/tools/out-of-tree/fs"
	"code.dumpstack.io/tools/out-of-tree/qemu"
)

type Client struct {
	RemoteAddr string
}

func (c Client) client() *tls.Conn {
	if !fs.PathExists(dotfiles.File("daemon/cert.pem")) {
		log.Fatal().Msgf("no {cert,key}.pem at %s",
			dotfiles.Dir("daemon"))
	}

	cert, err := tls.LoadX509KeyPair(
		dotfiles.File("daemon/cert.pem"),
		dotfiles.File("daemon/key.pem"))
	if err != nil {
		log.Fatal().Err(err).Msg("")
	}

	cacert, err := os.ReadFile(dotfiles.File("daemon/cert.pem"))
	if err != nil {
		log.Fatal().Err(err).Msg("")
	}
	certpool := x509.NewCertPool()
	certpool.AppendCertsFromPEM(cacert)

	tlscfg := &tls.Config{
		RootCAs:      certpool,
		Certificates: []tls.Certificate{cert},
	}

	conn, err := tls.Dial("tcp", c.RemoteAddr, tlscfg)
	if err != nil {
		log.Fatal().Err(err).Msg("")
	}

	return conn // conn.Close()
}

func (c Client) request(cmd api.Command, data any) (resp api.Resp, err error) {
	req := api.Req{Command: cmd}
	if data != nil {
		req.SetData(data)
	}

	conn := c.client()
	defer conn.Close()

	req.Encode(conn)

	err = resp.Decode(conn)
	if err != nil {
		log.Fatal().Err(err).Msgf("request %v", req)
	}

	log.Debug().Msgf("resp: %v", resp)

	if resp.Error != "" {
		err = errors.New(resp.Error)
		log.Fatal().Err(err).Msg("")
	}

	return
}

func (c Client) Jobs(params api.ListJobsParams) (jobs []api.Job, err error) {
	resp, _ := c.request(api.ListJobs, &params)

	err = resp.GetData(&jobs)
	if err != nil {
		log.Error().Err(err).Msg("")
	}

	return
}

func (c Client) AddJob(job api.Job) (uuid string, err error) {
	resp, err := c.request(api.AddJob, &job)
	if err != nil {
		return
	}

	err = resp.GetData(&uuid)
	return
}

func (c Client) Repos() (repos []api.Repo, err error) {
	resp, _ := c.request(api.ListRepos, nil)

	log.Debug().Msgf("resp: %v", spew.Sdump(resp))

	err = resp.GetData(&repos)
	if err != nil {
		log.Error().Err(err).Msg("")
	}

	return
}

type logWriter struct {
	tag string
}

func (lw logWriter) Write(p []byte) (n int, err error) {
	n = len(p)
	log.Trace().Str("tag", lw.tag).Msgf("%v", strconv.Quote(string(p)))
	return
}

func (c Client) handler(cConn net.Conn) {
	defer cConn.Close()

	dConn := c.client()
	defer dConn.Close()

	req := api.Req{Command: api.RawMode}
	req.Encode(dConn)

	go io.Copy(cConn, io.TeeReader(dConn, logWriter{"recv"}))
	io.Copy(dConn, io.TeeReader(cConn, logWriter{"send"}))
}

var ErrRepoNotFound = errors.New("repo not found")

// GetRepo virtual API call
func (c Client) GetRepo(name string) (repo api.Repo, err error) {
	// TODO add API call

	repos, err := c.Repos()
	if err != nil {
		return
	}

	for _, r := range repos {
		if r.Name == name {
			repo = r
			return
		}
	}

	err = ErrRepoNotFound
	return
}

func (c Client) GitProxy(addr string, ready *sync.Mutex) {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal().Err(err).Msg("git proxy listen")
	}
	defer l.Close()

	log.Debug().Msgf("git proxy listen on %v", addr)

	for {
		ready.Unlock()
		conn, err := l.Accept()
		if err != nil {
			log.Fatal().Err(err).Msg("accept")
		}
		log.Debug().Msgf("git proxy accept %s", conn.RemoteAddr())

		go c.handler(conn)
	}
}

func (c Client) PushRepo(repo api.Repo) (err error) {
	addr := qemu.GetFreeAddrPort()

	ready := &sync.Mutex{}

	ready.Lock()
	go c.GitProxy(addr, ready)

	ready.Lock()

	remote := fmt.Sprintf("git://%s/%s", addr, repo.Name)
	log.Debug().Msgf("git proxy remote: %v", remote)

	raw, err := exec.Command("git", "--work-tree", repo.Path, "push", "--force", remote).
		CombinedOutput()
	if err != nil {
		return
	}

	log.Info().Msgf("push repo %v\n%v", repo, string(raw))
	return
}

func (c Client) AddRepo(repo api.Repo) (err error) {
	_, err = c.request(api.AddRepo, &repo)
	if err != nil {
		return
	}

	log.Info().Msgf("add repo %v", repo)
	return
}

func (c Client) Kernels() (kernels []distro.KernelInfo, err error) {
	resp, err := c.request(api.Kernels, nil)
	if err != nil {
		return
	}

	err = resp.GetData(&kernels)
	if err != nil {
		log.Error().Err(err).Msg("")
	}

	log.Info().Msgf("got %d kernels", len(kernels))
	return
}

func (c Client) JobStatus(uuid string) (st api.Status, err error) {
	resp, err := c.request(api.JobStatus, &uuid)
	if err != nil {
		return
	}

	err = resp.GetData(&st)
	if err != nil {
		log.Error().Err(err).Msg("")
	}

	return
}

func (c Client) JobLogs(uuid string) (logs []api.JobLog, err error) {
	resp, err := c.request(api.JobLogs, &uuid)
	if err != nil {
		return
	}

	err = resp.GetData(&logs)
	if err != nil {
		log.Error().Err(err).Msg("")
	}

	return
}