package daemon

import (
	"crypto/tls"
	"database/sql"
	"io"
	"net"
	"os/exec"
	"runtime"
	"sync"
	"time"

	"github.com/remeh/sizedwaitgroup"
	"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/daemon/db"
	"code.dumpstack.io/tools/out-of-tree/fs"
)

type Daemon struct {
	Threads   int
	Resources *Resources

	db           *sql.DB
	kernelConfig string

	shutdown bool
	wg       sync.WaitGroup
}

func Init(kernelConfig string) (d *Daemon, err error) {
	d = &Daemon{}
	d.Threads = runtime.NumCPU()
	d.Resources = NewResources()

	d.kernelConfig = kernelConfig

	d.wg.Add(1) // matches with db.Close()
	d.db, err = db.OpenDatabase(dotfiles.File("daemon/daemon.db"))
	if err != nil {
		log.Error().Err(err).Msg("cannot open daemon.db")
	}

	log.Info().Msgf("database %s", dotfiles.File("daemon/daemon.db"))
	return
}

func (d *Daemon) Kill() {
	d.shutdown = true

	d.db.Close()
	d.wg.Done()
}

func (d *Daemon) Daemon() {
	if d.db == nil {
		log.Fatal().Msg("db is not initialized")
	}

	swg := sizedwaitgroup.New(d.Threads)
	log.Info().Int("threads", d.Threads).Msg("start")

	first := true

	for !d.shutdown {
		d.wg.Add(1)

		jobs, err := db.Jobs(d.db, "")
		if err != nil && !d.shutdown {
			log.Error().Err(err).Msg("")
			d.wg.Done()
			time.Sleep(time.Minute)
			continue
		}

		for _, job := range jobs {
			if d.shutdown {
				break
			}

			pj := newJobProcessor(job, d.db)

			if first && job.Status == api.StatusRunning {
				pj.SetStatus(api.StatusWaiting)
				continue
			}

			if job.Status == api.StatusNew {
				pj.SetStatus(api.StatusWaiting)
				continue
			}

			if job.Status != api.StatusWaiting {
				continue
			}

			swg.Add()
			go func(pj jobProcessor) {
				defer swg.Done()
				pj.Process(d.Resources)
				time.Sleep(time.Second)
			}(pj)
		}

		first = false

		d.wg.Done()
		time.Sleep(time.Second)
	}

	swg.Wait()
}

func handler(conn net.Conn, e cmdenv) {
	defer conn.Close()

	resp := api.NewResp()

	e.Log = log.With().
		Str("resp_uuid", resp.UUID).
		Str("remote_addr", conn.RemoteAddr().String()).
		Logger()

	e.Log.Info().Msg("")

	var req api.Req

	defer func() {
		if req.Command != api.RawMode {
			resp.Encode(conn)
		} else {
			log.Debug().Msg("raw mode, not encode response")
		}
	}()

	err := req.Decode(conn)
	if err != nil {
		e.Log.Error().Err(err).Msg("cannot decode")
		return
	}

	err = command(&req, &resp, e)
	if err != nil {
		e.Log.Error().Err(err).Msg("")
		return
	}
}

func (d *Daemon) Listen(addr string) {
	if d.db == nil {
		log.Fatal().Msg("db is not initialized")
	}

	go func() {
		repodir := dotfiles.Dir("daemon/repos")
		git := exec.Command("git", "daemon", "--port=9418", "--verbose",
			"--reuseaddr",
			"--export-all", "--base-path="+repodir,
			"--enable=receive-pack",
			"--enable=upload-pack",
			repodir)

		stdout, err := git.StdoutPipe()
		if err != nil {
			log.Fatal().Err(err).Msgf("%v", git)
			return
		}

		go io.Copy(logWriter{log: log.Logger}, stdout)

		stderr, err := git.StderrPipe()
		if err != nil {
			log.Fatal().Err(err).Msgf("%v", git)
			return
		}

		go io.Copy(logWriter{log: log.Logger}, stderr)

		log.Debug().Msgf("start %v", git)
		git.Start()
		defer func() {
			log.Debug().Msgf("stop %v", git)
		}()

		err = git.Wait()
		if err != nil {
			log.Fatal().Err(err).Msgf("%v", git)
			return
		}
	}()

	if !fs.PathExists(dotfiles.File("daemon/cert.pem")) {
		log.Info().Msg("No cert.pem, generating...")
		cmd := exec.Command("openssl",
			"req", "-batch", "-newkey", "rsa:2048",
			"-new", "-nodes", "-x509",
			"-subj", "/CN=*",
			"-addext", "subjectAltName = DNS:*",
			"-out", dotfiles.File("daemon/cert.pem"),
			"-keyout", dotfiles.File("daemon/key.pem"))

		out, err := cmd.Output()
		if err != nil {
			log.Error().Err(err).Msg(string(out))
			return
		}
	}

	log.Info().Msg("copy to client:")
	log.Info().Msgf("cert: %s, key: %s",
		dotfiles.File("daemon/cert.pem"),
		dotfiles.File("daemon/key.pem"))

	cert, err := tls.LoadX509KeyPair(dotfiles.File("daemon/cert.pem"),
		dotfiles.File("daemon/key.pem"))
	if err != nil {
		log.Fatal().Err(err).Msg("LoadX509KeyPair")
	}
	tlscfg := &tls.Config{Certificates: []tls.Certificate{cert}}

	l, err := tls.Listen("tcp", addr, tlscfg)
	if err != nil {
		log.Fatal().Err(err).Msg("listen")
	}

	log.Info().Str("addr", ":9418").Msg("git")
	log.Info().Str("addr", addr).Msg("daemon")

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

		e := cmdenv{
			DB:           d.db,
			WG:           &d.wg,
			Conn:         conn,
			KernelConfig: d.kernelConfig,
		}

		go handler(conn, e)
	}
}