// Package mospkg provides functionality for Mosis app packages (.mosis files)
package mospkg

import (
	"archive/zip"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
)

// KeyPair represents an Ed25519 signing keypair
type KeyPair struct {
	PrivateKey ed25519.PrivateKey
	PublicKey  ed25519.PublicKey
}

// GenerateKeyPair generates a new Ed25519 keypair
func GenerateKeyPair() (*KeyPair, error) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, fmt.Errorf("generate key: %w", err)
	}
	return &KeyPair{
		PrivateKey: privateKey,
		PublicKey:  publicKey,
	}, nil
}

// Fingerprint returns the SHA256 fingerprint of the public key
func (kp *KeyPair) Fingerprint() string {
	hash := sha256.Sum256(kp.PublicKey)
	return fmt.Sprintf("SHA256:%s", base64.StdEncoding.EncodeToString(hash[:]))
}

// PrivateKeyPEM returns the private key in PEM format
func (kp *KeyPair) PrivateKeyPEM() ([]byte, error) {
	pkcs8, err := x509.MarshalPKCS8PrivateKey(kp.PrivateKey)
	if err != nil {
		return nil, fmt.Errorf("marshal private key: %w", err)
	}
	return pem.EncodeToMemory(&pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: pkcs8,
	}), nil
}

// PublicKeyPEM returns the public key in PEM format
func (kp *KeyPair) PublicKeyPEM() ([]byte, error) {
	pkix, err := x509.MarshalPKIXPublicKey(kp.PublicKey)
	if err != nil {
		return nil, fmt.Errorf("marshal public key: %w", err)
	}
	return pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pkix,
	}), nil
}

// LoadPrivateKey loads an Ed25519 private key from PEM data
func LoadPrivateKey(pemData []byte) (ed25519.PrivateKey, error) {
	block, _ := pem.Decode(pemData)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block")
	}

	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("parse private key: %w", err)
	}

	ed25519Key, ok := key.(ed25519.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("key is not Ed25519")
	}

	return ed25519Key, nil
}

// LoadPublicKey loads an Ed25519 public key from PEM data
func LoadPublicKey(pemData []byte) (ed25519.PublicKey, error) {
	block, _ := pem.Decode(pemData)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block")
	}

	key, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("parse public key: %w", err)
	}

	ed25519Key, ok := key.(ed25519.PublicKey)
	if !ok {
		return nil, fmt.Errorf("key is not Ed25519")
	}

	return ed25519Key, nil
}

// PublicKeyFingerprint returns the SHA256 fingerprint of a public key
func PublicKeyFingerprint(publicKey ed25519.PublicKey) string {
	hash := sha256.Sum256(publicKey)
	return fmt.Sprintf("SHA256:%s", base64.StdEncoding.EncodeToString(hash[:]))
}

// GenerateManifestMF generates MANIFEST.MF content for a package
func GenerateManifestMF(packagePath string) ([]byte, error) {
	reader, err := zip.OpenReader(packagePath)
	if err != nil {
		return nil, fmt.Errorf("open package: %w", err)
	}
	defer reader.Close()

	var lines []string
	lines = append(lines, "Manifest-Version: 1.0")
	lines = append(lines, "Created-By: mosis-portal")
	lines = append(lines, "")

	// Sort files for consistent ordering
	var fileNames []string
	for _, file := range reader.File {
		if file.FileInfo().IsDir() {
			continue
		}
		// Skip META-INF files
		if strings.HasPrefix(file.Name, "META-INF/") {
			continue
		}
		fileNames = append(fileNames, file.Name)
	}
	sort.Strings(fileNames)

	// Generate hash for each file
	for _, name := range fileNames {
		for _, file := range reader.File {
			if file.Name != name {
				continue
			}

			rc, err := file.Open()
			if err != nil {
				return nil, fmt.Errorf("open file %s: %w", name, err)
			}

			hasher := sha256.New()
			if _, err := io.Copy(hasher, rc); err != nil {
				rc.Close()
				return nil, fmt.Errorf("hash file %s: %w", name, err)
			}
			rc.Close()

			digest := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
			lines = append(lines, fmt.Sprintf("Name: %s", name))
			lines = append(lines, fmt.Sprintf("SHA-256-Digest: %s", digest))
			lines = append(lines, "")
		}
	}

	return []byte(strings.Join(lines, "\n")), nil
}

// SignManifest signs MANIFEST.MF content with an Ed25519 private key
func SignManifest(manifestMF []byte, privateKey ed25519.PrivateKey) []byte {
	return ed25519.Sign(privateKey, manifestMF)
}

// VerifySignature verifies a signature against MANIFEST.MF using a public key
func VerifySignature(manifestMF, signature []byte, publicKey ed25519.PublicKey) bool {
	return ed25519.Verify(publicKey, manifestMF, signature)
}

// SignPackage signs a .mosis package by adding META-INF/MANIFEST.MF and META-INF/CERT.SIG
func SignPackage(packagePath, outputPath string, privateKey ed25519.PrivateKey) error {
	// Generate MANIFEST.MF
	manifestMF, err := GenerateManifestMF(packagePath)
	if err != nil {
		return fmt.Errorf("generate manifest: %w", err)
	}

	// Sign manifest
	signature := SignManifest(manifestMF, privateKey)

	// Open source package
	srcReader, err := zip.OpenReader(packagePath)
	if err != nil {
		return fmt.Errorf("open source package: %w", err)
	}
	defer srcReader.Close()

	// Create output package
	outFile, err := os.Create(outputPath)
	if err != nil {
		return fmt.Errorf("create output: %w", err)
	}
	defer outFile.Close()

	writer := zip.NewWriter(outFile)
	defer writer.Close()

	// Copy existing files (except META-INF)
	for _, file := range srcReader.File {
		if strings.HasPrefix(file.Name, "META-INF/") {
			continue
		}

		// Copy file to new archive
		destFile, err := writer.CreateHeader(&file.FileHeader)
		if err != nil {
			return fmt.Errorf("create header: %w", err)
		}

		srcFile, err := file.Open()
		if err != nil {
			return fmt.Errorf("open source file: %w", err)
		}

		if _, err := io.Copy(destFile, srcFile); err != nil {
			srcFile.Close()
			return fmt.Errorf("copy file: %w", err)
		}
		srcFile.Close()
	}

	// Add META-INF/MANIFEST.MF
	manifestWriter, err := writer.Create("META-INF/MANIFEST.MF")
	if err != nil {
		return fmt.Errorf("create MANIFEST.MF: %w", err)
	}
	if _, err := manifestWriter.Write(manifestMF); err != nil {
		return fmt.Errorf("write MANIFEST.MF: %w", err)
	}

	// Add META-INF/CERT.SIG (base64 encoded signature)
	sigWriter, err := writer.Create("META-INF/CERT.SIG")
	if err != nil {
		return fmt.Errorf("create CERT.SIG: %w", err)
	}
	if _, err := sigWriter.Write([]byte(base64.StdEncoding.EncodeToString(signature))); err != nil {
		return fmt.Errorf("write CERT.SIG: %w", err)
	}

	return nil
}

// VerifyPackageSignature verifies the signature of a signed .mosis package
func VerifyPackageSignature(packagePath string, publicKey ed25519.PublicKey) (bool, error) {
	reader, err := zip.OpenReader(packagePath)
	if err != nil {
		return false, fmt.Errorf("open package: %w", err)
	}
	defer reader.Close()

	// Find MANIFEST.MF and CERT.SIG
	var manifestMF []byte
	var signature []byte

	for _, file := range reader.File {
		switch file.Name {
		case "META-INF/MANIFEST.MF":
			rc, err := file.Open()
			if err != nil {
				return false, fmt.Errorf("open MANIFEST.MF: %w", err)
			}
			manifestMF, err = io.ReadAll(rc)
			rc.Close()
			if err != nil {
				return false, fmt.Errorf("read MANIFEST.MF: %w", err)
			}

		case "META-INF/CERT.SIG":
			rc, err := file.Open()
			if err != nil {
				return false, fmt.Errorf("open CERT.SIG: %w", err)
			}
			sigB64, err := io.ReadAll(rc)
			rc.Close()
			if err != nil {
				return false, fmt.Errorf("read CERT.SIG: %w", err)
			}
			signature, err = base64.StdEncoding.DecodeString(string(sigB64))
			if err != nil {
				return false, fmt.Errorf("decode signature: %w", err)
			}
		}
	}

	if manifestMF == nil {
		return false, fmt.Errorf("MANIFEST.MF not found")
	}
	if signature == nil {
		return false, fmt.Errorf("CERT.SIG not found")
	}

	// Verify signature
	if !VerifySignature(manifestMF, signature, publicKey) {
		return false, nil
	}

	// Verify file hashes
	hashes, err := parseManifestMF(manifestMF)
	if err != nil {
		return false, fmt.Errorf("parse MANIFEST.MF: %w", err)
	}

	for _, file := range reader.File {
		if file.FileInfo().IsDir() || strings.HasPrefix(file.Name, "META-INF/") {
			continue
		}

		expectedHash, ok := hashes[file.Name]
		if !ok {
			return false, fmt.Errorf("file not in manifest: %s", file.Name)
		}

		rc, err := file.Open()
		if err != nil {
			return false, fmt.Errorf("open file %s: %w", file.Name, err)
		}

		hasher := sha256.New()
		if _, err := io.Copy(hasher, rc); err != nil {
			rc.Close()
			return false, fmt.Errorf("hash file %s: %w", file.Name, err)
		}
		rc.Close()

		actualHash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
		if actualHash != expectedHash {
			return false, fmt.Errorf("hash mismatch for %s", file.Name)
		}
	}

	return true, nil
}

// parseManifestMF parses MANIFEST.MF content and returns a map of filename -> SHA256 hash
func parseManifestMF(data []byte) (map[string]string, error) {
	hashes := make(map[string]string)
	lines := strings.Split(string(data), "\n")

	var currentFile string
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "Name: ") {
			currentFile = strings.TrimPrefix(line, "Name: ")
		} else if strings.HasPrefix(line, "SHA-256-Digest: ") && currentFile != "" {
			hashes[currentFile] = strings.TrimPrefix(line, "SHA-256-Digest: ")
			currentFile = ""
		}
	}

	return hashes, nil
}
