package jsonio

import (
	"bytes"
	"encoding/json"
	"io"
	"os"
)

// MarshalIndent renders v as indented JSON without HTML escaping.
//
// Terminated with the conventional trailing newline emitted by
// json.Encoder.Encode.
func MarshalIndent(v any) ([]byte, error) {
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// WriteFile marshals v as indented JSON (no HTML escaping) and writes it to
// path.
//
// Used for human-edited source artifacts (plan.json, tenants-*.json,
// manifest.json); for runtime state that must survive partial writes, use
// status.DumpJSONAtomic instead.
func WriteFile(path string, v any) error {
	data, err := MarshalIndent(v)
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o600)
}

// ReadFile reads path and unmarshals its JSON content into v.
//
// The symmetric counterpart to WriteFile; callers wrap the returned error
// (which is unwrapped by design, like WriteFile's) to attach the path.
func ReadFile(path string, v any) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, v)
}

// ReadFileStrict reads path and unmarshals its JSON content into v,
// rejecting unknown fields.
//
// For runtime inputs a human may hand-edit (tune files, request bodies)
// where a typo must surface instead of being silently ignored.
func ReadFileStrict(path string, v any) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return DecodeStrict(bytes.NewReader(data), v)
}

// DecodeStrict decodes JSON from r into v, rejecting unknown fields.
func DecodeStrict(r io.Reader, v any) error {
	dec := json.NewDecoder(r)
	dec.DisallowUnknownFields()
	return dec.Decode(v)
}

// WriteFull writes the entire buffer, returning io.ErrShortWrite on a
// partial write.
func WriteFull(w io.Writer, p []byte) error {
	n, err := w.Write(p)
	if err != nil {
		return err
	}
	if n != len(p) {
		return io.ErrShortWrite
	}
	return nil
}

// AppendSynced appends data to path and fsyncs before close.
//
// The sync is load-bearing for callers that gate further actions on the
// append having happened (job history vs in-flight markers): without it a
// power loss can drop an acknowledged append. Single-writer by contract.
func AppendSynced(path string, data []byte) error {
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	if err := WriteFull(f, data); err != nil {
		f.Close()
		return err
	}
	if err := f.Sync(); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}
