// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
//
// SPDX-License-Identifier: Apache-2.0

package minicheddar

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os/exec"
	"time"

	"github.com/google/safehtml"
	"github.com/google/safehtml/uncheckedconversions"
)

const startupMax = 10 * time.Second

type Cheddar struct {
	remote string
	stop   func()
}

type markdownReqResp struct {
	Data string `json:"data"`
}

func (c *Cheddar) Markdown(ctx context.Context, text string) (safehtml.HTML, error) {
	buf := new(bytes.Buffer)

	if err := json.NewEncoder(buf).Encode(markdownReqResp{Data: text}); err != nil {
		return safehtml.HTML{}, fmt.Errorf("encoding markdown rendering request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/markdown", c.remote), buf)
	if err != nil {
		return safehtml.HTML{}, fmt.Errorf("making markdown request object: %w", err)
	}
	req.Header.Set("Content-type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return safehtml.HTML{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return safehtml.HTML{}, fmt.Errorf("markdown rendering request returned status %d; not OK", resp.StatusCode)
	}

	var respData markdownReqResp
	if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
		return safehtml.HTML{}, fmt.Errorf("decoding markdown rendering response: %w", err)
	}

	return uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(respData.Data), nil
}

type codeReq struct {
	Filepath string `json:"filepath"`
	Theme    string `json:"theme"`
	Code     string `json:"code"`
}

type codeResp struct {
	IsPlaintext bool   `json:"is_plaintext"`
	Data        string `json:"data"`
}

func (c *Cheddar) Code(ctx context.Context, filename, theme, text string) (safehtml.HTML, error) {
	buf := new(bytes.Buffer)

	if err := json.NewEncoder(buf).Encode(codeReq{
		Filepath: filename,
		Theme:    theme,
		Code:     text,
	}); err != nil {
		return safehtml.HTML{}, fmt.Errorf("encoding code rendering request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s", c.remote), buf)
	if err != nil {
		return safehtml.HTML{}, fmt.Errorf("making code request object: %w", err)
	}
	req.Header.Set("Content-type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return safehtml.HTML{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return safehtml.HTML{}, fmt.Errorf("code rendering request returned status %d; not OK", resp.StatusCode)
	}

	var respData codeResp
	if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
		return safehtml.HTML{}, fmt.Errorf("decoding code rendering response: %w", err)
	}

	if respData.IsPlaintext {
		return safehtml.HTMLEscaped(respData.Data), nil
	}
	return uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(respData.Data), nil
}

func (c *Cheddar) healthCheck(ctx context.Context) error {
	_, err := c.Markdown(ctx, "")
	return err
}

func (c *Cheddar) Close() {
	if c.stop != nil {
		c.stop()
	}
}

// Remote connects to an already running instance of Cheddar.
func Remote(url string) *Cheddar {
	return &Cheddar{
		remote: url,
	}
}

// Spawn spawns a new instance of Cheddar listening on some port.
// This is potentially race-y, because it relies on binding, then unbinding and spawning Cheddar.
// But... it should be good enough.
func Spawn(ctx context.Context, cpath string) (*Cheddar, error) {
	l, err := net.Listen("tcp", "[::1]:0")
	if err != nil {
		return nil, fmt.Errorf("finding a port for cheddar: %w", err)
	}
	addr := l.Addr().(*net.TCPAddr)
	port := addr.Port
	if err := l.Close(); err != nil {
		return nil, fmt.Errorf("closing port-sentinel listener: %w", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cmd := exec.CommandContext(ctx, cpath, "--sourcegraph-server", "--listen", fmt.Sprintf("[::1]:%d", port))
	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("launching cheddar: %w", err)
	}
	c := &Cheddar{
		remote: fmt.Sprintf("http://[::1]:%d", port),
		stop:   cancel,
	}

	startupCtx, startupCancel := context.WithTimeout(ctx, startupMax)
	defer startupCancel()

	// Give cheddar a bit to start up before we start healthchecking it.
	time.Sleep(1 * time.Millisecond)
	for {
		if err := c.healthCheck(startupCtx); errors.Is(err, context.DeadlineExceeded) {
			cancel() // Kill the subprocess.
			return nil, err
		} else if err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	return c, nil
}