package mfer

import (
	"bytes"
	"crypto/sha256"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/klauspost/compress/zstd"
	"google.golang.org/protobuf/proto"
)

// MAGIC is the file format magic bytes prefix (rot13 of "MANIFEST").
const MAGIC string = "ZNAVSRFG"

func newTimestampFromTime(t time.Time) *Timestamp {
	out := &Timestamp{
		Seconds: t.Unix(),
		Nanos:   int32(t.UnixNano() - (t.Unix() * 1000000000)),
	}
	return out
}

func (m *manifest) generate() error {
	if m.pbInner == nil {
		return errors.New("internal error: pbInner not set")
	}
	if m.pbOuter == nil {
		e := m.generateOuter()
		if e != nil {
			return e
		}
	}
	dat, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbOuter)
	if err != nil {
		return err
	}
	m.output = bytes.NewBuffer([]byte(MAGIC))
	_, err = m.output.Write(dat)
	if err != nil {
		return err
	}
	return nil
}

func (m *manifest) generateOuter() error {
	if m.pbInner == nil {
		return errors.New("internal error")
	}

	// Generate UUID and set on inner message
	manifestUUID := uuid.New()
	m.pbInner.Uuid = manifestUUID[:]

	innerData, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbInner)
	if err != nil {
		return err
	}

	// Compress the inner data
	idc := new(bytes.Buffer)
	zw, err := zstd.NewWriter(idc, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
	if err != nil {
		return err
	}
	_, err = zw.Write(innerData)
	if err != nil {
		return err
	}
	_ = zw.Close()

	compressedData := idc.Bytes()

	// Hash the compressed data for integrity verification before decompression
	h := sha256.New()
	if _, err := h.Write(compressedData); err != nil {
		return err
	}
	sha256Hash := h.Sum(nil)

	m.pbOuter = &MFFileOuter{
		InnerMessage:    compressedData,
		Size:            int64(len(innerData)),
		Sha256:          sha256Hash,
		Uuid:            manifestUUID[:],
		Version:         MFFileOuter_VERSION_ONE,
		CompressionType: MFFileOuter_COMPRESSION_ZSTD,
	}

	// Sign the manifest if signing options are provided
	if m.signingOptions != nil && m.signingOptions.KeyID != "" {
		sigString, err := m.signatureString()
		if err != nil {
			return fmt.Errorf("failed to generate signature string: %w", err)
		}

		sig, err := gpgSign([]byte(sigString), m.signingOptions.KeyID)
		if err != nil {
			return fmt.Errorf("failed to sign manifest: %w", err)
		}
		m.pbOuter.Signature = sig

		fingerprint, err := gpgGetKeyFingerprint(m.signingOptions.KeyID)
		if err != nil {
			return fmt.Errorf("failed to get key fingerprint: %w", err)
		}
		m.pbOuter.Signer = fingerprint

		pubKey, err := gpgExportPublicKey(m.signingOptions.KeyID)
		if err != nil {
			return fmt.Errorf("failed to export public key: %w", err)
		}
		m.pbOuter.SigningPubKey = pubKey
	}

	return nil
}
