package zddc

import (
	"fmt"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v3"
)

// WriteAtomic writes data to absPath atomically. The parent directory is
// created (mode 0o755) if it does not exist; the file is written with mode
// 0o644.
//
// Implementation: bytes go to a sibling temp file, are fsync'd, then renamed
// onto absPath. On any failure the temp is removed and absPath is untouched.
// Knows nothing about caches — callers that need cache invalidation
// (.zddc, the apps cascade, etc.) handle it themselves.
func WriteAtomic(absPath string, data []byte) error {
	dir := filepath.Dir(absPath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("ensure dir: %w", err)
	}

	base := filepath.Base(absPath)
	tmp, err := os.CreateTemp(dir, "."+base+".*.tmp")
	if err != nil {
		return fmt.Errorf("create temp: %w", err)
	}
	tmpPath := tmp.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()

	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return fmt.Errorf("write temp: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return fmt.Errorf("fsync temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close temp: %w", err)
	}
	if err := os.Chmod(tmpPath, 0o644); err != nil {
		return fmt.Errorf("chmod temp: %w", err)
	}
	if err := os.Rename(tmpPath, absPath); err != nil {
		return fmt.Errorf("rename: %w", err)
	}
	return nil
}

// WriteFile atomically writes zf as YAML to <dirPath>/.zddc.
//
// The YAML round-trips through Marshal then Unmarshal as a sanity check —
// this catches struct-encoding bugs before they hit disk and ensures the
// file we produce is parseable by ParseFile (which is what every reader
// uses). On any failure the original file is untouched.
//
// After the write succeeds the policy and scan caches for dirPath (and
// descendants) are invalidated so the next EffectivePolicy / ScanZddcFiles
// call reads fresh content.
func WriteFile(dirPath string, zf ZddcFile) error {
	dirPath = filepath.Clean(dirPath)

	data, err := yaml.Marshal(&zf)
	if err != nil {
		return fmt.Errorf("marshal: %w", err)
	}

	// Sanity round-trip: re-parse what we just produced. If this fails the
	// in-memory struct does not survive a write/read cycle and we should
	// abort before touching disk.
	var probe ZddcFile
	if err := yaml.Unmarshal(data, &probe); err != nil {
		return fmt.Errorf("round-trip parse: %w", err)
	}

	target := filepath.Join(dirPath, ".zddc")
	if err := WriteAtomic(target, data); err != nil {
		return err
	}

	InvalidateCache(dirPath)
	InvalidateScanCache()
	return nil
}

// DeleteFile removes <dirPath>/.zddc. Returns nil if the file does not exist.
// Cache invalidation runs unconditionally so any in-memory copy of an old
// chain is dropped.
func DeleteFile(dirPath string) error {
	dirPath = filepath.Clean(dirPath)
	target := filepath.Join(dirPath, ".zddc")
	err := os.Remove(target)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("remove: %w", err)
	}
	InvalidateCache(dirPath)
	InvalidateScanCache()
	return nil
}
