package hashvault

import (
	"bytes"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	b64 "encoding/base64"
	"encoding/json"
	"encoding/pem"
	"io"
	"net/http"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/httpclient"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/fiskerinc/cloud-services/pkg/validator"
	"github.com/pkg/errors"
)

var (
	vaultToken         = envtool.GetEnv("VAULT_TOKEN", "REPLACE_ME")
	vaultURL           = envtool.GetEnv("VAULT_URL", "REPLACE_ME")
	vaultIssueEndpath  = envtool.GetEnv("ISSUE_PATH", "REPLACE_ME")
	vaultRevokeEndpath = envtool.GetEnv("REVOKE_PATH", "REPLACE_ME")
	vaultRenewEndpath  = envtool.GetEnv("RENEW_PATH", "REPLACE_ME")
	vaultPki           = envtool.GetEnv("VAULT_PKI", "/pki")
	organization       = "Fisker, Inc"
	organization_unit  = "Edge Compute"
	vaultClientOnce    sync.Once
	vaultClient        VaultInterface
)

const (
	vaultHeader = "X-Vault-Token"
	cert_format = "pem"
)

type VaultInterface interface {
	CreateCertificate(commonName string, certificateType string, isEU bool) (*common.Certificate, error)
	CreatePKICertificate(commonName string) (*common.Certificate, error)
	RevokeCertificate(serial string, certType string) (*common.Certificate, error)
	RenewCertificate(commonName string, privateKey string, certType string) (*common.Certificate, error)
}

func GetVaultClient() VaultInterface {
	vaultClientOnce.Do((func() {
		if vaultClient == nil {
			vaultClient = &Vault{}
		}
	}))
	return vaultClient
}

func SetVaultClient(client VaultInterface) {
	vaultClient = client
}

type VaultCreateCert struct {
	Common string `json:"common_name" validate:"required,max=64"`
	Format string `json:"format"`
}

type Vault struct {
}

func (vault *Vault) CreateCertificate(commonName string, certificateType string, isEU bool) (*common.Certificate, error) {
	url := vaultURLPath(certificateType) + getIssuePath(commonName, isEU)

	postBody := VaultCreateCert{
		Common: commonName,
		Format: cert_format,
	}

	err := validator.ValidateStruct(postBody)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return vault.createCertificate(url, commonName, certificateType, postBody)
}

func (vault *Vault) CreatePKICertificate(commonName string) (*common.Certificate, error) {
	url := vaultURL + "/pki-manufacture/issue/fisker"

	postBody := VaultCreateCert{
		Common: commonName,
		Format: cert_format,
	}

	err := validator.ValidateStruct(postBody)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	// TODO: clarify with John Wu if cert type is properly used.
	return vault.createCertificate(url, commonName, "rsa", postBody)
}

func (vault *Vault) createCertificate(vaultURL, commonName, certType string, postBody any) (*common.Certificate, error) {
	postHeader := http.Header{}
	postHeader.Set(vaultHeader, vaultToken)
	client := &http.Client{Timeout: 60 * time.Second}
	jsonBytes, err := json.Marshal(postBody)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	request, err := http.NewRequest(http.MethodPost, vaultURL, bytes.NewReader(jsonBytes))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	request.Header = postHeader
	resp, err := client.Do(request)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()

	if resp != nil && resp.StatusCode != 200 {
		return nil, ErrorCertificateCreation(resp)
	}
	var vaultResponse VaultResponse
	err = json.NewDecoder(resp.Body).Decode(&vaultResponse)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	var certificateData = vaultResponse.CertificateData
	response := common.Certificate{
		PublicKey:    certificateData.Certificate,
		CommonName:   commonName,
		PrivateKey:   certificateData.PrivateKey,
		SerialNumber: certificateData.SerialNumber,
		EncryptedKey: []byte(certificateData.PrivateKey),
		Type:         certType,
		Valid:        true,
	}
	return &response, nil
}

func (vault *Vault) RevokeCertificate(serial string, certType string) (*common.Certificate, error) {
	url := vaultURLPath(certType)

	postBody := struct {
		Serial string `json:"serial_number"`
	}{
		Serial: serial,
	}
	postHeader := http.Header{}
	postHeader.Set(vaultHeader, vaultToken)
	resp, err := httpclient.Post(url+vaultRevokeEndpath, postBody, postHeader)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp != nil && resp.StatusCode != 200 {
		return nil, ErrorCertificateCreation(resp)
	}
	response := common.Certificate{
		SerialNumber: serial,
		Valid:        false,
	}
	return &response, nil
}

func (vault *Vault) RenewCertificate(commonName string, privateKey string, certType string) (*common.Certificate, error) {
	url := vaultURLPath(certType)
	csrBytes, err := GenerateCsr(commonName, privateKey)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	postBody := struct {
		CSR    string `json:"csr"`
		Common string `json:"common_name"`
		Format string `json:"format"`
	}{
		CSR:    FormatCsr(csrBytes),
		Common: commonName,
		Format: cert_format,
	}
	postHeader := http.Header{}
	postHeader.Set(vaultHeader, vaultToken)
	resp, err := httpclient.Post(url+vaultRenewEndpath, postBody, postHeader)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	if resp != nil && resp.StatusCode != 200 {
		return nil, ErrorCertificateCreation(resp)
	}
	var vaultResponse VaultResponse
	var certificateData CertificateData
	err = json.NewDecoder(resp.Body).Decode(&vaultResponse)
	certificateData = vaultResponse.CertificateData

	response := common.Certificate{
		PublicKey:    certificateData.Certificate,
		CommonName:   commonName,
		SerialNumber: certificateData.SerialNumber,
		Valid:        true,
		Type:         certType,
	}
	return &response, err
}

func vaultURLPath(certType string) string {
	url := vaultURL

	switch certType {
	case common.CertAftersales:
		return url + "/pki-aftersales"
	default:
		return url + vaultPki
	}
}

func getIssuePath(vin string, isEU bool) string {
	if isEU {
		return vaultIssueEndpath + "-eu"
	}

	err := validator.GetValidator().Var(vin, "vin,vincheck")
	if err != nil {
		return vaultIssueEndpath
	}

	switch vin[6:7] {
	case "E":
		return vaultIssueEndpath + "-eu"
	default:
		return vaultIssueEndpath
	}
}

func FormatCsr(csrBytes []byte) string {
	csr := b64.StdEncoding.EncodeToString([]byte(csrBytes))
	for i := 0; i < len(csr); i += 64 {
		csr = csr[:i] + "\n" + csr[i:]
	}
	return "-----BEGIN CERTIFICATE REQUEST-----" + csr + "\n-----END CERTIFICATE REQUEST-----"
}

func GenerateCsr(vin string, privateKey string) ([]byte, error) {
	subj := pkix.Name{
		CommonName:         vin,
		Organization:       []string{organization},
		OrganizationalUnit: []string{organization_unit},
	}

	template := x509.CertificateRequest{
		Subject:            subj,
		SignatureAlgorithm: x509.SHA256WithRSA,
	}

	block, _ := pem.Decode([]byte(privateKey))
	priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, priv)
	return csrBytes, err
}

func readRespString(resp *http.Response) string {
	if resp == nil {
		return ""
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		logger.Warn().Err(err).Send()
		return ""
	}

	return string(data)
}

func ErrorCertificateCreation(resp *http.Response) error {
	err := readRespString(resp)
	return errors.Errorf("Vault unable to create certificate, status code: %d %s", resp.StatusCode, err)
}

type VaultResponse struct {
	RequestID       string          `json:"request_id"`
	LeaseID         string          `json:"lease_id"`
	Renewable       bool            `json:"renewable"`
	LeaseDuration   int             `json:"lease_duration"`
	CertificateData CertificateData `json:"data"`
}

type CertificateData struct {
	Certificate    string `json:"certificate"`
	Expiration     int    `json:"expiration"`
	IssuingCA      string `json:"issuing_ca"`
	PrivateKey     string `json:"private_key"`
	PrivateKeyType string `json:"private_key_type"`
	SerialNumber   string `json:"serial_number"`
}
