package hscontrol

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/chasefleming/elem-go/styles"
	"github.com/gorilla/mux"
	"github.com/juanfont/headscale/hscontrol/templates"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/rs/zerolog/log"
	"tailscale.com/tailcfg"
)

const (
	// The CapabilityVersion is used by Tailscale clients to indicate
	// their codebase version. Tailscale clients can communicate over TS2021
	// from CapabilityVersion 28, but we only have good support for it
	// since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port).
	//
	// Related to this change, there is https://github.com/tailscale/tailscale/pull/5379,
	// where CapabilityVersion 39 is introduced to indicate #4323 was merged.
	//
	// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
	NoiseCapabilityVersion = 39

	reservedResponseHeaderSize = 4
)

// httpError logs an error and sends an HTTP error response with the given
func httpError(w http.ResponseWriter, err error) {
	var herr HTTPError
	if errors.As(err, &herr) {
		http.Error(w, herr.Msg, herr.Code)
		log.Error().Err(herr.Err).Int("code", herr.Code).Msgf("user msg: %s", herr.Msg)
	} else {
		http.Error(w, "internal server error", http.StatusInternalServerError)
		log.Error().Err(err).Int("code", http.StatusInternalServerError).Msg("http internal server error")
	}
}

// HTTPError represents an error that is surfaced to the user via web.
type HTTPError struct {
	Code int    // HTTP response code to send to client; 0 means 500
	Msg  string // Response body to send to client
	Err  error  // Detailed error to log on the server
}

func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]: %s, %s", e.Code, e.Msg, e.Err) }
func (e HTTPError) Unwrap() error { return e.Err }

// Error returns an HTTPError containing the given information.
func NewHTTPError(code int, msg string, err error) HTTPError {
	return HTTPError{Code: code, Msg: msg, Err: err}
}

var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)

var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
	"machines registered with CLI does not support expire",
)
var ErrNoCapabilityVersion = errors.New("no capability version set")

func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
	clientCapabilityStr := req.URL.Query().Get("v")

	if clientCapabilityStr == "" {
		return 0, NewHTTPError(http.StatusBadRequest, "capability version must be set", nil)
	}

	clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
	if err != nil {
		return 0, NewHTTPError(http.StatusBadRequest, "invalid capability version", fmt.Errorf("failed to parse capability version: %w", err))
	}

	return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
}

func (h *Headscale) derpRequestIsAllowed(
	req *http.Request,
) (bool, error) {
	body, err := io.ReadAll(req.Body)
	if err != nil {
		return false, fmt.Errorf("cannot read request body: %w", err)
	}

	var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
	if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
		return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
	}

	nodes, err := h.db.ListNodes()
	if err != nil {
		return false, fmt.Errorf("cannot list nodes: %w", err)
	}

	return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil
}

// see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159, Derp use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
func (h *Headscale) VerifyHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	if req.Method != http.MethodPost {
		httpError(writer, errMethodNotAllowed)
		return
	}

	allow, err := h.derpRequestIsAllowed(req)
	if err != nil {
		httpError(writer, err)
		return
	}

	resp := tailcfg.DERPAdmitClientResponse{
		Allow: allow,
	}

	writer.Header().Set("Content-Type", "application/json")
	json.NewEncoder(writer).Encode(resp)
}

// KeyHandler provides the Headscale pub key
// Listens in /key.
func (h *Headscale) KeyHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
	capVer, err := parseCabailityVersion(req)
	if err != nil {
		httpError(writer, err)
		return
	}

	// TS2021 (Tailscale v2 protocol) requires to have a different key
	if capVer >= NoiseCapabilityVersion {
		resp := tailcfg.OverTLSPublicKeyResponse{
			PublicKey: h.noisePrivateKey.Public(),
		}
		writer.Header().Set("Content-Type", "application/json")
		json.NewEncoder(writer).Encode(resp)

		return
	}
}

func (h *Headscale) HealthHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	respond := func(err error) {
		writer.Header().Set("Content-Type", "application/health+json; charset=utf-8")

		res := struct {
			Status string `json:"status"`
		}{
			Status: "pass",
		}

		if err != nil {
			writer.WriteHeader(http.StatusInternalServerError)
			res.Status = "fail"
		}

		json.NewEncoder(writer).Encode(res)
	}

	if err := h.db.PingDB(req.Context()); err != nil {
		respond(err)

		return
	}

	respond(nil)
}

var codeStyleRegisterWebAPI = styles.Props{
	styles.Display:         "block",
	styles.Padding:         "20px",
	styles.Border:          "1px solid #bbb",
	styles.BackgroundColor: "#eee",
}

type AuthProviderWeb struct {
	serverURL string
}

func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
	return &AuthProviderWeb{
		serverURL: serverURL,
	}
}

func (a *AuthProviderWeb) AuthURL(registrationId types.RegistrationID) string {
	return fmt.Sprintf(
		"%s/register/%s",
		strings.TrimSuffix(a.serverURL, "/"),
		registrationId.String())
}

// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register/:registration_id.
//
// This is not part of the Tailscale control API, as we could send whatever URL
// in the RegisterResponse.AuthURL field.
func (a *AuthProviderWeb) RegisterHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	vars := mux.Vars(req)
	registrationIdStr := vars["registration_id"]

	// We need to make sure we dont open for XSS style injections, if the parameter that
	// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
	// the template and log an error.
	registrationId, err := types.RegistrationIDFromString(registrationIdStr)
	if err != nil {
		httpError(writer, NewHTTPError(http.StatusBadRequest, "invalid registration id", err))
		return
	}

	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
	writer.WriteHeader(http.StatusOK)
	writer.Write([]byte(templates.RegisterWeb(registrationId).Render()))
}
