package platform

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/Eriyc/rules_wails/pkg/wails3kit/updates"
)

func extractBundle(_ context.Context, archivePath string, format updates.ArtifactFormat) (string, updates.BundleManifest, error) {
	stageDir, err := os.MkdirTemp("", "wails3kit-stage-*")
	if err != nil {
		return "", updates.BundleManifest{}, err
	}

	switch format {
	case updates.ArtifactFormatZip:
		err = extractZip(archivePath, stageDir)
	case updates.ArtifactFormatTarGz:
		err = extractTarGz(archivePath, stageDir)
	default:
		err = fmt.Errorf("%w: unsupported format %s", updates.ErrInvalidArtifact, format)
	}
	if err != nil {
		_ = os.RemoveAll(stageDir)
		return "", updates.BundleManifest{}, err
	}

	manifestPath := filepath.Join(stageDir, "bundle.json")
	bytes, err := os.ReadFile(manifestPath)
	if err != nil {
		_ = os.RemoveAll(stageDir)
		return "", updates.BundleManifest{}, err
	}

	var manifest updates.BundleManifest
	if err := json.Unmarshal(bytes, &manifest); err != nil {
		_ = os.RemoveAll(stageDir)
		return "", updates.BundleManifest{}, err
	}

	if err := validateBundle(stageDir, manifest); err != nil {
		_ = os.RemoveAll(stageDir)
		return "", updates.BundleManifest{}, err
	}

	return stageDir, manifest, nil
}

func extractZip(archivePath string, targetDir string) error {
	reader, err := zip.OpenReader(archivePath)
	if err != nil {
		return err
	}
	defer reader.Close()

	for _, file := range reader.File {
		path, err := safeArchivePath(targetDir, file.Name)
		if err != nil {
			return err
		}
		if file.FileInfo().IsDir() {
			if err := os.MkdirAll(path, 0o755); err != nil {
				return err
			}
			continue
		}

		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return err
		}

		source, err := file.Open()
		if err != nil {
			return err
		}
		if err := writeFile(path, source, file.Mode()); err != nil {
			_ = source.Close()
			return err
		}
		_ = source.Close()
	}

	return nil
}

func extractTarGz(archivePath string, targetDir string) error {
	file, err := os.Open(archivePath)
	if err != nil {
		return err
	}
	defer file.Close()

	gzipReader, err := gzip.NewReader(file)
	if err != nil {
		return err
	}
	defer gzipReader.Close()

	reader := tar.NewReader(gzipReader)
	for {
		header, err := reader.Next()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}

		path, err := safeArchivePath(targetDir, header.Name)
		if err != nil {
			return err
		}

		switch header.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(path, 0o755); err != nil {
				return err
			}
		case tar.TypeReg:
			if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
				return err
			}
			if err := writeFile(path, reader, os.FileMode(header.Mode)); err != nil {
				return err
			}
		default:
			return fmt.Errorf("%w: unsupported tar entry %s", updates.ErrInvalidArtifact, header.Name)
		}
	}
}

func validateBundle(stageDir string, manifest updates.BundleManifest) error {
	if manifest.SchemaVersion != 1 {
		return fmt.Errorf("%w: unsupported bundle schema %d", updates.ErrInvalidArtifact, manifest.SchemaVersion)
	}
	if !isSafeRelative(manifest.EntryPoint) {
		return fmt.Errorf("%w: invalid entrypoint", updates.ErrInvalidArtifact)
	}
	if len(manifest.Files) == 0 {
		return fmt.Errorf("%w: bundle contains no files", updates.ErrInvalidArtifact)
	}

	for _, file := range manifest.Files {
		if !isSafeRelative(file.Path) {
			return fmt.Errorf("%w: invalid bundle path %s", updates.ErrInvalidArtifact, file.Path)
		}
		if _, err := strconv.ParseUint(file.Mode, 8, 32); err != nil {
			return fmt.Errorf("%w: invalid mode %s", updates.ErrInvalidArtifact, file.Mode)
		}
		info, err := os.Stat(filepath.Join(stageDir, filepath.FromSlash(file.Path)))
		if err != nil {
			return err
		}
		if info.IsDir() {
			return fmt.Errorf("%w: file entry %s is a directory", updates.ErrInvalidArtifact, file.Path)
		}
	}

	return nil
}

func safeArchivePath(root string, name string) (string, error) {
	if !isSafeRelative(name) && strings.TrimSpace(name) != "bundle.json" {
		return "", fmt.Errorf("%w: unsafe archive path %s", updates.ErrInvalidArtifact, name)
	}
	return filepath.Join(root, filepath.FromSlash(name)), nil
}

func isSafeRelative(path string) bool {
	cleaned := filepath.Clean(filepath.FromSlash(path))
	if cleaned == "." || cleaned == "" {
		return false
	}
	if filepath.IsAbs(cleaned) {
		return false
	}
	return !strings.HasPrefix(cleaned, "..")
}
