// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package ova implements OVA creation.
package ova

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"github.com/siderolabs/go-cmd/pkg/cmd"

	"github.com/siderolabs/talos/pkg/imager/utils"
	"github.com/siderolabs/talos/pkg/imager/vmdkconvert"
)

const mfTpl = `SHA256({{ .VMDK }})= {{ .VMDKSHA }}
SHA256({{ .OVF }})= {{ .OVFSHA }}
`

// OVF format reference: https://www.dmtf.org/standards/ovf.
//
//nolint:lll
const ovfTpl = `<?xml version="1.0" encoding="UTF-8"?>
<!--Generated by VMware ovftool 4.3.0 (build-7948156), UTC time: 2019-10-31T01:41:10.540841Z-->
<!-- Edited by Talos -->
<Envelope vmw:buildId="build-7948156" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <References>
    <File ovf:href="{{ .VMDK }}" ovf:id="file1" ovf:size="{{ .Size }}"/>
  </References>
  <DiskSection>
    <Info>Virtual disk information</Info>
    <Disk ovf:capacity="{{ .Capacity }}" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
  </DiskSection>
  <NetworkSection>
    <Info>The list of logical networks</Info>
    <Network ovf:name="VM Network">
      <Description>The VM Network network</Description>
    </Network>
  </NetworkSection>
  <VirtualSystem ovf:id="vm">
    <Info>A virtual machine</Info>
    <Name>talos</Name>
    <ProductSection ovf:required="false">
      <Info>Talos Virtual Appliance</Info>
      <Property ovf:userConfigurable="true" ovf:type="string"
                ovf:key="talos.config" ovf:value="">
        <Label>Talos config data</Label>
        <Description>Inline Talos config</Description>
      </Property>
    </ProductSection>
    <OperatingSystemSection ovf:id="101" vmw:osType="other3xLinux64Guest">
      <Info>The kind of installed guest operating system</Info>
    </OperatingSystemSection>
    <VirtualHardwareSection ovf:transport="com.vmware.guestInfo">
      <Info>Virtual hardware requirements</Info>
      <System>
        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
        <vssd:InstanceID>0</vssd:InstanceID>
        <vssd:VirtualSystemIdentifier>talos</vssd:VirtualSystemIdentifier>
        <vssd:VirtualSystemType>vmx-15</vssd:VirtualSystemType>
      </System>
      <Item>
        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
        <rasd:Description>Number of Virtual CPUs</rasd:Description>
        <rasd:ElementName>2 virtual CPU(s)</rasd:ElementName>
        <rasd:InstanceID>1</rasd:InstanceID>
        <rasd:ResourceType>3</rasd:ResourceType>
        <rasd:VirtualQuantity>2</rasd:VirtualQuantity>
      </Item>
      <Item>
        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
        <rasd:Description>Memory Size</rasd:Description>
        <rasd:ElementName>2048MB of memory</rasd:ElementName>
        <rasd:InstanceID>2</rasd:InstanceID>
        <rasd:ResourceType>4</rasd:ResourceType>
        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
      </Item>
      <Item>
        <rasd:Address>0</rasd:Address>
        <rasd:Description>SCSI Controller</rasd:Description>
        <rasd:ElementName>scsiController0</rasd:ElementName>
        <rasd:InstanceID>3</rasd:InstanceID>
        <rasd:ResourceSubType>VirtualSCSI</rasd:ResourceSubType>
        <rasd:ResourceType>6</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:AddressOnParent>0</rasd:AddressOnParent>
        <rasd:ElementName>disk0</rasd:ElementName>
        <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
        <rasd:InstanceID>4</rasd:InstanceID>
        <rasd:Parent>3</rasd:Parent>
        <rasd:ResourceType>17</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:AddressOnParent>2</rasd:AddressOnParent>
        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
        <rasd:Connection>VM Network</rasd:Connection>
        <rasd:Description>VmxNet3 ethernet adapter on &quot;VM Network&quot;</rasd:Description>
        <rasd:ElementName>ethernet0</rasd:ElementName>
        <rasd:InstanceID>5</rasd:InstanceID>
        <rasd:ResourceSubType>VmxNet3</rasd:ResourceSubType>
        <rasd:ResourceType>10</rasd:ResourceType>
        <vmw:Config ovf:required="false" vmw:key="slotInfo.pciSlotNumber" vmw:value="32"/>
        <vmw:Config ovf:required="false" vmw:key="wakeOnLanEnabled" vmw:value="false"/>
        <vmw:Config ovf:required="false" vmw:key="connectable.allowGuestControl" vmw:value="false"/>
      </Item>
      <Item ovf:required="false">
        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
        <rasd:ElementName>video</rasd:ElementName>
        <rasd:InstanceID>6</rasd:InstanceID>
        <rasd:ResourceType>24</rasd:ResourceType>
      </Item>
      <Item ovf:required="false">
        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
        <rasd:ElementName>vmci</rasd:ElementName>
        <rasd:InstanceID>7</rasd:InstanceID>
        <rasd:ResourceSubType>vmware.vmci</rasd:ResourceSubType>
        <rasd:ResourceType>1</rasd:ResourceType>
      </Item>
      <vmw:Config ovf:required="false" vmw:key="tools.syncTimeWithHost" vmw:value="false"/>
      <vmw:Config ovf:required="false" vmw:key="tools.afterPowerOn" vmw:value="true"/>
      <vmw:Config ovf:required="false" vmw:key="tools.afterResume" vmw:value="true"/>
      <vmw:Config ovf:required="false" vmw:key="tools.beforeGuestShutdown" vmw:value="true"/>
      <vmw:Config ovf:required="false" vmw:key="tools.beforeGuestStandby" vmw:value="true"/>
      <vmw:Config ovf:required="false" vmw:key="tools.toolsUpgradePolicy" vmw:value="manual"/>
      <vmw:Config ovf:required="false" vmw:key="powerOpInfo.suspendType" vmw:value="soft"/>
      <vmw:ExtraConfig ovf:required="false" vmw:key="nvram" vmw:value="talos.nvram"/>
      <vmw:ExtraConfig ovf:required="false" vmw:key="disk.EnableUUID" vmw:value="true"/>
    </VirtualHardwareSection>
  </VirtualSystem>
</Envelope>
`

// CreateOVAFromRAW creates an OVA from a RAW disk.
//
//nolint:gocyclo
func CreateOVAFromRAW(ctx context.Context, outPath, arch, scratchPath string, diskSize int64, printf func(string, ...any)) error {
	if err := os.MkdirAll(scratchPath, 0o755); err != nil {
		return err
	}

	vmdkPath := filepath.Join(scratchPath, "disk.vmdk")

	if err := utils.CopyFiles(printf, utils.SourceDestination(outPath, vmdkPath)); err != nil {
		return err
	}

	if err := vmdkconvert.ConvertToStreamOptimizedVMDK(ctx, vmdkPath, printf); err != nil {
		return err
	}

	f, err := os.Stat(vmdkPath)
	if err != nil {
		return err
	}

	imageSize := f.Size()

	ovf, err := renderOVF(imageSize, diskSize)
	if err != nil {
		return err
	}

	input, err := os.Open(vmdkPath)
	if err != nil {
		return err
	}

	defer input.Close() //nolint:errcheck

	vmdkSHA25Sum, err := sha256sum(input)
	if err != nil {
		return err
	}

	ovfSHA25Sum, err := sha256sum(strings.NewReader(ovf))
	if err != nil {
		return err
	}

	mf, err := renderMF(vmdkSHA25Sum, ovfSHA25Sum)
	if err != nil {
		return err
	}

	if err = os.WriteFile(filepath.Join(scratchPath, "disk.mf"), []byte(mf), 0o666); err != nil {
		return err
	}

	if err = os.WriteFile(filepath.Join(scratchPath, "disk.ovf"), []byte(ovf), 0o666); err != nil {
		return err
	}

	if _, err = cmd.RunWithOptions(ctx, "tar", []string{"-cvf", outPath, "-C", scratchPath, "disk.ovf", "disk.mf", "disk.vmdk"}); err != nil {
		return err
	}

	return nil
}

func sha256sum(input io.Reader) (string, error) {
	hash := sha256.New()

	if _, err := io.Copy(hash, input); err != nil {
		return "", err
	}

	sum := hash.Sum(nil)

	return hex.EncodeToString(sum), nil
}

func renderMF(vmdkSHA25Sum, ovfSHA25Sum string) (string, error) {
	cfg := struct {
		VMDK    string
		VMDKSHA string
		OVF     string
		OVFSHA  string
	}{
		VMDK:    "disk.vmdk",
		VMDKSHA: vmdkSHA25Sum,
		OVF:     "disk.ovf",
		OVFSHA:  ovfSHA25Sum,
	}

	templ := template.Must(template.New("mf").Parse(mfTpl))

	var buf bytes.Buffer

	if err := templ.Execute(&buf, cfg); err != nil {
		return "", err
	}

	return buf.String(), nil
}

func renderOVF(imageSize, diskSize int64) (string, error) {
	cfg := struct {
		VMDK     string
		Size     int64
		Capacity int64
	}{
		VMDK:     "disk.vmdk",
		Size:     imageSize,
		Capacity: diskSize / (1 << 20),
	}

	templ := template.Must(template.New("ovf").Parse(ovfTpl))

	var buf bytes.Buffer

	if err := templ.Execute(&buf, cfg); err != nil {
		return "", err
	}

	return buf.String(), nil
}
