package client

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"

	cs "fn-registry/functions/cybersecurity"

	"github.com/oklog/ulid/v2"
)

// newULID returns a fresh, lexicographically-sortable message id with
// crypto/rand entropy.
func newULID() string {
	return ulid.MustNew(ulid.Now(), rand.Reader).String()
}

// identityFile is the on-disk JSON representation of an Identity. The four key
// fields are base64-encoded.
//
// SECURITY: this file contains the peer's long-term PRIVATE keys (SignPriv and
// KexPriv). It is written 0600. Losing it means losing the ability to decrypt
// any message addressed to this endpoint — there is no recovery. Treat it like
// an SSH private key. (Hardening with OS keyrings/HSM is a later phase.)
type identityFile struct {
	SignPub  string `json:"sign_pub"`
	SignPriv string `json:"sign_priv"`
	KexPub   string `json:"kex_pub"`
	KexPriv  string `json:"kex_priv"`
}

// LoadIdentity loads an existing identity from path. Unlike LoadOrCreateIdentity
// it NEVER creates one: a missing or unreadable file is an error. It is for
// callers that must consume a specific, pre-provisioned identity rather than mint
// a fresh one — for example membershipd's persisted internal service identity,
// which `membershipd user add --store kv` reads to present the privileged nkey
// the cluster authenticator recognizes.
func LoadIdentity(path string) (cs.Identity, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return cs.Identity{}, fmt.Errorf("client: read identity %q: %w", path, err)
	}
	var f identityFile
	if err := json.Unmarshal(data, &f); err != nil {
		return cs.Identity{}, fmt.Errorf("client: parse identity %q: %w", path, err)
	}
	id, err := f.toIdentity()
	if err != nil {
		return cs.Identity{}, fmt.Errorf("client: decode identity %q: %w", path, err)
	}
	return id, nil
}

// LoadOrCreateIdentity loads the identity at path, or generates and persists a
// new one if the file does not exist. The file is written with 0600
// permissions because it holds private keys. A file that exists but is
// unreadable or corrupt is an error (NOT silently regenerated), so a damaged
// identity surfaces instead of minting a new key that cannot decrypt old data.
func LoadOrCreateIdentity(path string) (cs.Identity, error) {
	if _, statErr := os.Stat(path); statErr == nil {
		return LoadIdentity(path)
	}

	id, err := cs.GenerateIdentity()
	if err != nil {
		return cs.Identity{}, fmt.Errorf("client: generate identity: %w", err)
	}
	if err := saveIdentity(path, id); err != nil {
		return cs.Identity{}, err
	}
	return id, nil
}

func saveIdentity(path string, id cs.Identity) error {
	if dir := filepath.Dir(path); dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("client: mkdir for identity: %w", err)
		}
	}
	f := identityFile{
		SignPub:  base64.StdEncoding.EncodeToString(id.SignPub),
		SignPriv: base64.StdEncoding.EncodeToString(id.SignPriv),
		KexPub:   base64.StdEncoding.EncodeToString(id.KexPub),
		KexPriv:  base64.StdEncoding.EncodeToString(id.KexPriv),
	}
	data, err := json.MarshalIndent(f, "", "  ")
	if err != nil {
		return fmt.Errorf("client: marshal identity: %w", err)
	}
	if err := os.WriteFile(path, data, 0o600); err != nil {
		return fmt.Errorf("client: write identity %q: %w", path, err)
	}
	return nil
}

func (f identityFile) toIdentity() (cs.Identity, error) {
	dec := func(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(s) }
	signPub, err := dec(f.SignPub)
	if err != nil {
		return cs.Identity{}, err
	}
	signPriv, err := dec(f.SignPriv)
	if err != nil {
		return cs.Identity{}, err
	}
	kexPub, err := dec(f.KexPub)
	if err != nil {
		return cs.Identity{}, err
	}
	kexPriv, err := dec(f.KexPriv)
	if err != nil {
		return cs.Identity{}, err
	}
	return cs.Identity{SignPub: signPub, SignPriv: signPriv, KexPub: kexPub, KexPriv: kexPriv}, nil
}
