package main

import (
	"encoding/json"
	"os"
	"time"
)

// The ledger
// ==========
//
// <root>/ledger.jsonl is one JSON object per line, appended and never
// rewritten. It is the answer to "what did this thing actually do to my
// machine", and it has to survive the program crashing, the machine losing
// power, and somebody reading it while it is being written -- so it is opened
// O_APPEND, written in a single Write call per record, and closed. No
// existing byte is ever read back, moved or altered.
//
// Deleting a line is possible with a text editor, obviously. Append-only here
// means this program never removes one.

// LedgerRecord is one line of the ledger.
type LedgerRecord struct {
	TS      string `json:"ts"`
	Action  string `json:"action"`
	Slug    string `json:"slug,omitempty"`
	Name    string `json:"name,omitempty"`
	Version string `json:"version,omitempty"`
	From    string `json:"from_version,omitempty"`
	SHA256  string `json:"sha256,omitempty"`
	Size    int64  `json:"size,omitempty"`
	Path    string `json:"path,omitempty"`
	Trash   string `json:"trash,omitempty"`
	Source  string `json:"source,omitempty"`
	Result  string `json:"result"`
	Detail  string `json:"detail,omitempty"`
}

// appendLedger writes one record. A failure to write the ledger is reported
// but never rolls anything back: the change already happened, and losing the
// note about it is less bad than pretending it did not.
func (r *Root) appendLedger(rec LedgerRecord) error {
	if rec.TS == "" {
		rec.TS = time.Now().UTC().Format(time.RFC3339Nano)
	}
	b, err := json.Marshal(rec)
	if err != nil {
		return err
	}
	b = append(b, '\n')
	f, err := os.OpenFile(r.ledgerPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
	if err != nil {
		return err
	}
	if _, err := f.Write(b); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}

// readLedger parses the whole ledger. Lines that will not parse are reported
// as a synthetic record rather than aborting the read, so one bad line does
// not hide the rest of the history.
func (r *Root) readLedger() ([]LedgerRecord, error) {
	b, err := os.ReadFile(r.ledgerPath())
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	var out []LedgerRecord
	start := 0
	for i := 0; i <= len(b); i++ {
		if i != len(b) && b[i] != '\n' {
			continue
		}
		line := b[start:i]
		start = i + 1
		if len(line) == 0 {
			continue
		}
		var rec LedgerRecord
		if err := json.Unmarshal(line, &rec); err != nil {
			out = append(out, LedgerRecord{Action: "unreadable-line", Result: "error", Detail: string(line)})
			continue
		}
		out = append(out, rec)
	}
	return out, nil
}
