package export

// format.go — v0.34.0 CS0
//
// Defines the .switchboard archive format: a zip file containing a
// manifest.json and per-entity JSON files under data/. File blobs
// live under files/{id}/{filename}.

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"
	"time"
)

// ── Constants ────────────────────────────────

const (
	FormatVersion        = 1
	MaxExportArchiveSize = 500 * 1024 * 1024  // 500 MB total
	MaxExportFileSize    = 100 * 1024 * 1024  // 100 MB per file blob
	MaxExportFiles       = 10000
	ExportContentType    = "application/zip"
	ExportExtension      = ".switchboard"
)

// ── Manifest ─────────────────────────────────

// Manifest is the top-level metadata written to manifest.json.
type Manifest struct {
	Version       string         `json:"version"`        // server version, e.g. "0.34.0"
	FormatVersion int            `json:"format_version"` // always 1 for now
	ExportType    string         `json:"export_type"`    // "user", "team"
	CreatedAt     time.Time      `json:"created_at"`
	ExportedBy    string         `json:"exported_by"`    // user ID
	EntityCounts  map[string]int `json:"entity_counts"`
	Scope         *ManifestScope `json:"scope,omitempty"`
}

// ManifestScope narrows the export to a user or team.
type ManifestScope struct {
	UserID string `json:"user_id,omitempty"`
	TeamID string `json:"team_id,omitempty"`
}

// ── ArchiveWriter ────────────────────────────

// ArchiveWriter wraps zip.Writer with size tracking and convenience
// methods for writing manifest and entity JSON files.
type ArchiveWriter struct {
	zw       *zip.Writer
	written  int64
	maxBytes int64
}

// NewArchiveWriter creates an ArchiveWriter that streams to w.
func NewArchiveWriter(w io.Writer) *ArchiveWriter {
	return &ArchiveWriter{
		zw:       zip.NewWriter(w),
		maxBytes: MaxExportArchiveSize,
	}
}

// WriteManifest serializes m as indented JSON into manifest.json.
func (aw *ArchiveWriter) WriteManifest(m *Manifest) error {
	data, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		return fmt.Errorf("export: marshal manifest: %w", err)
	}
	return aw.writeEntry("manifest.json", data)
}

// WriteEntityJSON serializes data as indented JSON into data/{name}.json.
func (aw *ArchiveWriter) WriteEntityJSON(name string, data interface{}) (int, error) {
	buf, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return 0, fmt.Errorf("export: marshal %s: %w", name, err)
	}
	if err := aw.writeEntry("data/"+name+".json", buf); err != nil {
		return 0, err
	}
	// Return entity count for manifest
	switch v := data.(type) {
	case []interface{}:
		return len(v), nil
	default:
		// Use json.RawMessage length heuristic — caller tracks count
		return 0, nil
	}
}

// WriteFile streams a file blob into files/{zipPath}. Returns bytes
// written or an error if the file exceeds MaxExportFileSize.
func (aw *ArchiveWriter) WriteFile(zipPath string, r io.Reader) (int64, error) {
	if aw.written+1 > aw.maxBytes {
		return 0, fmt.Errorf("export: archive size limit exceeded")
	}
	f, err := aw.zw.Create("files/" + zipPath)
	if err != nil {
		return 0, fmt.Errorf("export: create zip entry %s: %w", zipPath, err)
	}
	lr := io.LimitReader(r, MaxExportFileSize+1)
	n, err := io.Copy(f, lr)
	if err != nil {
		return n, fmt.Errorf("export: write file %s: %w", zipPath, err)
	}
	if n > MaxExportFileSize {
		return n, fmt.Errorf("export: file %s exceeds %d bytes", zipPath, MaxExportFileSize)
	}
	aw.written += n
	return n, nil
}

// Close finalizes the zip archive.
func (aw *ArchiveWriter) Close() error {
	return aw.zw.Close()
}

// BytesWritten returns the approximate bytes written so far.
func (aw *ArchiveWriter) BytesWritten() int64 {
	return aw.written
}

// ── ArchiveReader ────────────────────────────

// ArchiveReader wraps zip.ReadCloser with validation helpers.
type ArchiveReader struct {
	zr *zip.ReadCloser
}

// OpenArchive opens and validates a .switchboard zip archive.
func OpenArchive(path string) (*ArchiveReader, error) {
	zr, err := zip.OpenReader(path)
	if err != nil {
		return nil, fmt.Errorf("export: open archive: %w", err)
	}
	return &ArchiveReader{zr: zr}, nil
}

// ReadManifest reads and validates manifest.json from the archive.
func (ar *ArchiveReader) ReadManifest() (*Manifest, error) {
	for _, f := range ar.zr.File {
		if f.Name == "manifest.json" {
			rc, err := f.Open()
			if err != nil {
				return nil, fmt.Errorf("export: open manifest: %w", err)
			}
			defer rc.Close()
			var m Manifest
			if err := json.NewDecoder(rc).Decode(&m); err != nil {
				return nil, fmt.Errorf("export: decode manifest: %w", err)
			}
			if m.FormatVersion > FormatVersion {
				return nil, fmt.Errorf("export: unsupported format version %d (max %d)", m.FormatVersion, FormatVersion)
			}
			return &m, nil
		}
	}
	return nil, fmt.Errorf("export: manifest.json not found in archive")
}

// ReadEntityJSON reads data/{name}.json into target.
func (ar *ArchiveReader) ReadEntityJSON(name string, target interface{}) error {
	path := "data/" + name + ".json"
	for _, f := range ar.zr.File {
		if f.Name == path {
			rc, err := f.Open()
			if err != nil {
				return fmt.Errorf("export: open %s: %w", path, err)
			}
			defer rc.Close()
			if err := json.NewDecoder(rc).Decode(target); err != nil {
				return fmt.Errorf("export: decode %s: %w", path, err)
			}
			return nil
		}
	}
	// Entity file not present — not an error (sparse archives are valid)
	return nil
}

// FileEntries returns zip entries under files/.
func (ar *ArchiveReader) FileEntries() []*zip.File {
	var out []*zip.File
	for _, f := range ar.zr.File {
		if len(f.Name) > 6 && f.Name[:6] == "files/" && !f.FileInfo().IsDir() {
			out = append(out, f)
		}
	}
	return out
}

// Close closes the underlying zip reader.
func (ar *ArchiveReader) Close() error {
	return ar.zr.Close()
}

// writeEntry writes raw bytes as a zip entry, tracking total size.
func (aw *ArchiveWriter) writeEntry(name string, data []byte) error {
	if aw.written+int64(len(data)) > aw.maxBytes {
		return fmt.Errorf("export: archive size limit exceeded writing %s", name)
	}
	f, err := aw.zw.Create(name)
	if err != nil {
		return fmt.Errorf("export: create entry %s: %w", name, err)
	}
	n, err := f.Write(data)
	aw.written += int64(n)
	return err
}
