package hscontrol

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

	"github.com/juanfont/headscale/hscontrol/assets"
	"github.com/juanfont/headscale/hscontrol/templates"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/rs/zerolog/log"
	"tailscale.com/tailcfg"
)

const (
	// NoiseCapabilityVersion 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) {
	if herr, ok := errors.AsType[HTTPError](err); ok {
		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 }

// NewHTTPError 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 do not support expiry",
)

func parseCapabilityVersion(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("parsing capability version: %w", err))
	}

	return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
}

func (h *Headscale) handleVerifyRequest(
	req *http.Request,
	writer io.Writer,
) error {
	body, err := io.ReadAll(req.Body)
	if err != nil {
		return fmt.Errorf("reading request body: %w", err)
	}

	var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
	if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil { //nolint:noinlineerr
		return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("parsing DERP client request: %w", err))
	}

	nodes := h.state.ListNodes()

	// Check if any node has the requested NodeKey
	var nodeKeyFound bool

	for _, node := range nodes.All() {
		if node.NodeKey() == derpAdmitClientRequest.NodePublic {
			nodeKeyFound = true
			break
		}
	}

	resp := &tailcfg.DERPAdmitClientResponse{
		Allow: nodeKeyFound,
	}

	return json.NewEncoder(writer).Encode(resp)
}

// VerifyHandler 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
	}

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

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

// 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 := parseCapabilityVersion(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")

		err := json.NewEncoder(writer).Encode(resp)
		if err != nil {
			log.Error().Err(err).Msg("failed to encode public key response")
		}

		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"
		}

		encErr := json.NewEncoder(writer).Encode(res)
		if encErr != nil {
			log.Error().Err(encErr).Msg("failed to encode health response")
		}
	}

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

		return
	}

	respond(nil)
}

func (h *Headscale) RobotsHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	writer.Header().Set("Content-Type", "text/plain")
	writer.WriteHeader(http.StatusOK)

	_, err := writer.Write([]byte("User-agent: *\nDisallow: /"))
	if err != nil {
		log.Error().
			Caller().
			Err(err).
			Msg("Failed to write HTTP response")
	}
}

// VersionHandler returns version information about the Headscale server
// Listens in /version.
func (h *Headscale) VersionHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(http.StatusOK)

	versionInfo := types.GetVersionInfo()

	err := json.NewEncoder(writer).Encode(versionInfo)
	if err != nil {
		log.Error().
			Caller().
			Err(err).
			Msg("Failed to write version response")
	}
}

type AuthProviderWeb struct {
	serverURL string
}

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

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

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

func (a *AuthProviderWeb) AuthHandler(
	writer http.ResponseWriter,
	req *http.Request,
) {
	authID, err := authIDFromRequest(req)
	if err != nil {
		httpError(writer, err)
		return
	}

	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
	writer.WriteHeader(http.StatusOK)

	_, err = writer.Write([]byte(templates.AuthWeb(
		"Authentication check",
		"Run the command below in the headscale server to approve this authentication request:",
		"headscale auth approve --auth-id "+authID.String(),
	).Render()))
	if err != nil {
		log.Error().Err(err).Msg("failed to write auth response")
	}
}

func authIDFromRequest(req *http.Request) (types.AuthID, error) {
	raw, err := urlParam[string](req, "auth_id")
	if err != nil {
		return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err))
	}

	// 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.
	authId, err := types.AuthIDFromString(raw)
	if err != nil {
		return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err))
	}

	return authId, nil
}

// RegisterHandler 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,
) {
	authId, err := authIDFromRequest(req)
	if err != nil {
		httpError(writer, err)
		return
	}

	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
	writer.WriteHeader(http.StatusOK)

	_, err = writer.Write([]byte(templates.AuthWeb(
		"Node registration",
		"Run the command below in the headscale server to add this node to your network:",
		fmt.Sprintf("headscale auth register --auth-id %s --user USERNAME", authId.String()),
	).Render()))
	if err != nil {
		log.Error().Err(err).Msg("failed to write register response")
	}
}

func FaviconHandler(writer http.ResponseWriter, req *http.Request) {
	writer.Header().Set("Content-Type", "image/png")
	http.ServeContent(writer, req, "favicon.ico", time.Unix(0, 0), bytes.NewReader(assets.Favicon))
}

// BlankHandler returns a blank page with favicon linked.
func BlankHandler(writer http.ResponseWriter, res *http.Request) {
	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
	writer.WriteHeader(http.StatusOK)

	_, err := writer.Write([]byte(templates.BlankPage().Render()))
	if err != nil {
		log.Error().
			Caller().
			Err(err).
			Msg("Failed to write HTTP response")
	}
}
