package archive

import (
	"archive/tar"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"kisekinopureya.com.tr/updater/internal/version"
)

// Output metadata structure
type Output struct {
	Metadata struct {
		Date    int64  `json:"date"`
		Version string `json:"version"`
	} `json:"metadata"`
	Files []struct {
		Name   string `json:"name"`
		Type   string `json:"type"`
		Sha256 string `json:"sha256"`
	} `json:"files"`
}

type fileInfo struct {
	Name   string
	Type   string
	Path   string
	Sha256 string
}

// Pack creates a tar.xz archive with metadata.json and the given images
func Pack(rootfsPath, verityPath, kernelPath, outPath string) error {
	paths := []struct {
		Path string
		Type string
	}{
		{rootfsPath, "rootfs"},
		{verityPath, "dm-verity"},
		{kernelPath, "kernel"},
	}

	var infos []fileInfo
	for _, p := range paths {
		log.Printf("Hashing %s...", p.Path)
		sum, err := sha256File(p.Path)
		if err != nil {
			return fmt.Errorf("hashing %s: %w", p.Path, err)
		}
		infos = append(infos, fileInfo{
			Name:   filepath.Base(p.Path),
			Type:   p.Type,
			Path:   p.Path,
			Sha256: sum,
		})
	}

	output, err := exec.Command("blkid", rootfsPath).Output()
	if err != nil {
		log.Fatal(err)
	}

	versionFromImage, err := version.Parse(string(output))
	if err != nil {
		log.Fatal(err)
	}

	var out Output
	out.Metadata.Date = time.Now().Unix()
	// NOTE: version should be filled by caller if needed
	out.Metadata.Version = versionFromImage
	for _, fi := range infos {
		out.Files = append(out.Files, struct {
			Name   string `json:"name"`
			Type   string `json:"type"`
			Sha256 string `json:"sha256"`
		}{
			Name:   fi.Name,
			Type:   fi.Type,
			Sha256: fi.Sha256,
		})
	}

	metaBytes, err := json.MarshalIndent(out, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal metadata: %w", err)
	}

	log.Printf("Metadata: %+v\n", out)

	if err := writeTarXz(outPath, metaBytes, infos); err != nil {
		return fmt.Errorf("create tar.xz: %w", err)
	}

	sigFile := outPath + ".sig"
	cmd := exec.Command("gpg", "--output", sigFile, "--detach-sign", "--armor", outPath)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(fmt.Errorf("gpg signing failed: %w", err))
	}

	fmt.Printf("Created %s and signed as %s\n", outPath, sigFile)

	return nil
}

func sha256File(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

func writeTarXz(outPath string, metadata []byte, files []fileInfo) error {
	outFile, err := os.Create(outPath)
	if err != nil {
		return fmt.Errorf("create output: %w", err)
	}
	defer outFile.Close()

	cmd := exec.Command("xz", "-z", "-c")
	cmd.Stdout = outFile
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return err
	}

	if err := cmd.Start(); err != nil {
		return err
	}

	tw := tar.NewWriter(stdin)

	// metadata.json
	metaHeader := &tar.Header{
		Name: "metadata.json",
		Mode: 0644,
		Size: int64(len(metadata)),
	}
	if err := tw.WriteHeader(metaHeader); err != nil {
		return err
	}
	if _, err := tw.Write(metadata); err != nil {
		return err
	}

	// files
	for _, fi := range files {
		if err := addFileToTar(tw, fi.Path, fi.Name); err != nil {
			return err
		}
	}

	if err := tw.Close(); err != nil {
		return err
	}
	if err := stdin.Close(); err != nil {
		return err
	}
	if err := cmd.Wait(); err != nil {
		return err
	}

	return nil
}

func addFileToTar(tw *tar.Writer, srcPath, destName string) error {
	f, err := os.Open(srcPath)
	if err != nil {
		return err
	}
	defer f.Close()

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

	hdr := &tar.Header{
		Name:    destName,
		Mode:    int64(stat.Mode().Perm()),
		Size:    stat.Size(),
		ModTime: stat.ModTime(),
	}
	if err := tw.WriteHeader(hdr); err != nil {
		return err
	}

	_, err = io.Copy(tw, f)
	return err
}
