//go:build darwin || linux || windows

package libbox

import (
	"archive/zip"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"time"
)

type crashReportMetadata struct {
	reportMetadata
	CrashedAt       string `json:"crashedAt,omitempty"`
	SignalName      string `json:"signalName,omitempty"`
	SignalCode      string `json:"signalCode,omitempty"`
	ExceptionName   string `json:"exceptionName,omitempty"`
	ExceptionReason string `json:"exceptionReason,omitempty"`
}

func archiveCrashReport(path string, crashReportsDir string) {
	content, err := os.ReadFile(path)
	if err != nil || len(content) == 0 {
		return
	}

	info, _ := os.Stat(path)
	crashTime := time.Now().UTC()
	if info != nil {
		crashTime = info.ModTime().UTC()
	}

	initReportDir(crashReportsDir)
	destPath, err := nextAvailableReportPath(crashReportsDir, crashTime)
	if err != nil {
		return
	}
	initReportDir(destPath)

	writeReportFile(destPath, "go.log", content)
	metadata := crashReportMetadata{
		reportMetadata: baseReportMetadata(),
		CrashedAt:      crashTime.Format(time.RFC3339),
	}
	writeReportMetadata(destPath, metadata)
	os.Remove(path)
	copyConfigSnapshot(destPath)
}

func configSnapshotPath() string {
	return filepath.Join(sBasePath, "configuration.json")
}

func saveConfigSnapshot(configContent string) {
	snapshotPath := configSnapshotPath()
	os.WriteFile(snapshotPath, []byte(configContent), 0o666)
	chownReport(snapshotPath)
}

func redirectStderr(path string) error {
	crashReportsDir := filepath.Join(sWorkingPath, "crash_reports")
	archiveCrashReport(path, crashReportsDir)
	archiveCrashReport(path+".old", crashReportsDir)

	outputFile, err := os.Create(path)
	if err != nil {
		return err
	}
	if runtime.GOOS != "android" && runtime.GOOS != "windows" {
		err = outputFile.Chown(sUserID, sGroupID)
		if err != nil {
			outputFile.Close()
			os.Remove(outputFile.Name())
			return err
		}
	}

	err = debug.SetCrashOutput(outputFile, debug.CrashOptions{})
	if err != nil {
		outputFile.Close()
		os.Remove(outputFile.Name())
		return err
	}
	_ = outputFile.Close()
	return nil
}

func CreateZipArchive(sourcePath string, destinationPath string) error {
	sourceInfo, err := os.Stat(sourcePath)
	if err != nil {
		return err
	}
	if !sourceInfo.IsDir() {
		return os.ErrInvalid
	}

	destinationFile, err := os.Create(destinationPath)
	if err != nil {
		return err
	}
	defer func() {
		_ = destinationFile.Close()
	}()

	zipWriter := zip.NewWriter(destinationFile)

	rootName := filepath.Base(sourcePath)
	err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		relativePath, err := filepath.Rel(sourcePath, path)
		if err != nil {
			return err
		}
		if relativePath == "." {
			return nil
		}

		archivePath := filepath.ToSlash(filepath.Join(rootName, relativePath))
		if d.IsDir() {
			_, err = zipWriter.Create(archivePath + "/")
			return err
		}

		fileInfo, err := d.Info()
		if err != nil {
			return err
		}
		header, err := zip.FileInfoHeader(fileInfo)
		if err != nil {
			return err
		}
		header.Name = archivePath
		header.Method = zip.Deflate

		writer, err := zipWriter.CreateHeader(header)
		if err != nil {
			return err
		}

		sourceFile, err := os.Open(path)
		if err != nil {
			return err
		}

		_, err = io.Copy(writer, sourceFile)
		closeErr := sourceFile.Close()
		if err != nil {
			return err
		}
		return closeErr
	})
	if err != nil {
		_ = zipWriter.Close()
		return err
	}

	return zipWriter.Close()
}
