// Copyright 2025 Emiliano Spinella (eminwux)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package fs

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"path/filepath"

	"github.com/eminwux/kukeon/internal/apischeme"
	"github.com/eminwux/kukeon/internal/consts"
	"github.com/eminwux/kukeon/internal/errdefs"
	intmodel "github.com/eminwux/kukeon/internal/modelhub"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
)

// MetadataRoot returns the root of kukeon's on-disk metadata tree under the
// daemon's RunPath. All realm/space/stack/cell metadata lives beneath this
// directory; siblings of the RunPath (e.g. <RunPath>/bin staging the kuketty
// binary, or the .kukeon-instance.json file) stay outside it so the reconcile
// loop and the subnet allocator can walk the metadata tree without treating
// non-metadata subdirectories as realms.
func MetadataRoot(baseRunPath string) string {
	return filepath.Join(baseRunPath, consts.KukeonMetadataSubdir)
}

// RealmMetadataDir returns the metadata directory for the given realm.
func RealmMetadataDir(baseRunPath, realmName string) string {
	return filepath.Join(MetadataRoot(baseRunPath), realmName)
}

// RealmMetadataPath returns the metadata file path for the given realm.
func RealmMetadataPath(baseRunPath, realmName string) string {
	return filepath.Join(
		RealmMetadataDir(baseRunPath, realmName),
		consts.KukeonMetadataFile,
	)
}

// SpaceMetadataDir returns the metadata directory for the given space within a realm.
func SpaceMetadataDir(baseRunPath, realmName, spaceName string) string {
	return filepath.Join(RealmMetadataDir(baseRunPath, realmName), spaceName)
}

// SpaceMetadataPath returns the metadata file path for the given space within a realm.
func SpaceMetadataPath(baseRunPath, realmName, spaceName string) string {
	return filepath.Join(
		SpaceMetadataDir(baseRunPath, realmName, spaceName),
		consts.KukeonMetadataFile,
	)
}

// StackMetadataDir returns the metadata directory for the given stack within a realm and space.
func StackMetadataDir(baseRunPath, realmName, spaceName, stackName string) string {
	return filepath.Join(SpaceMetadataDir(baseRunPath, realmName, spaceName), stackName)
}

// StackMetadataPath returns the metadata file path for the given stack within a realm and space.
func StackMetadataPath(baseRunPath, realmName, spaceName, stackName string) string {
	return filepath.Join(
		StackMetadataDir(baseRunPath, realmName, spaceName, stackName),
		consts.KukeonMetadataFile,
	)
}

// CellMetadataDir returns the metadata directory for the given cell within a realm, space, and stack.
func CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	return filepath.Join(StackMetadataDir(baseRunPath, realmName, spaceName, stackName), cellName)
}

// CellMetadataPath returns the metadata file path for the given cell within a realm, space, and stack.
func CellMetadataPath(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	return filepath.Join(
		CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName),
		consts.KukeonMetadataFile,
	)
}

// CellEtcHostsPath returns the host-side path of the per-cell /etc/hosts file
// kukeond renders and bind-mounts into every container in the cell. Lives
// under the cell's metadata directory so cell teardown cleans it up.
func CellEtcHostsPath(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	return filepath.Join(
		CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName),
		"etc-hosts",
	)
}

// CellEtcHostnamePath returns the host-side path of the per-cell /etc/hostname
// file kukeond renders and bind-mounts into every container in the cell.
// Lives under the cell's metadata directory so cell teardown cleans it up.
func CellEtcHostnamePath(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	return filepath.Join(
		CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName),
		"etc-hostname",
	)
}

// ContainerMetadataDir returns the per-container metadata directory inside the
// owning cell's metadata tree. Used as the host anchor for per-container state
// such as the sbsh control socket.
func ContainerMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName),
		containerName,
	)
}

// ContainerTTYDir returns the host-side per-container directory that owns
// the sbsh terminal socket and its capture/log siblings. It is bind-mounted
// into the container at /run/kukeon/tty so that sbsh's unlink-and-recreate
// cycle on the socket inode stays host-visible.
func ContainerTTYDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName),
		consts.KukeonContainerTTYDir,
	)
}

// ContainerSocketPath returns the host-side path of the kuketty terminal
// socket inode for a container — the bind-mount source path the kuketty
// listener inside the container resolves to via /run/kukeon/tty/socket.
// This path can be arbitrarily long (it inherits the full metadata layout
// depth) and is NOT safe to pass to `connect(2)`; see
// ContainerSocketSymlinkPath for the SUN_PATH-safe handle that
// `kuke attach` uses.
func ContainerSocketPath(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerTTYDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName),
		consts.KukeonContainerSocketFile,
	)
}

// ContainerSocketSymlinkDir returns the per-RunPath directory that holds
// SUN_PATH-safe symlinks to per-container kuketty sockets. The daemon
// creates this directory once and stages one symlink per Attachable
// container inside it; cell / container teardown removes the symlinks but
// leaves the directory itself in place (idempotent — re-provisioning the
// same container regenerates its symlink).
func ContainerSocketSymlinkDir(baseRunPath string) string {
	return filepath.Join(baseRunPath, consts.KukeonSocketSymlinkSubdir)
}

// ContainerSocketSymlinkPath returns the SUN_PATH-safe host-side path
// `kuke attach` connects to. It is a short symlink staged by the daemon
// inside ContainerSocketSymlinkDir that resolves to the deep socket inode
// returned by ContainerSocketPath. Linux's sockaddr_un.sun_path is bounded
// at 108 bytes including the terminator (consts.KukeonMaxSocketPath = 107
// usable bytes); the deep metadata-rooted ContainerSocketPath overflows on
// long RunPaths or long realm/space/stack/cell IDs (issue #521), so
// every host-side `connect(2)` is routed through this short handle.
//
// The basename is a 16-hex (sha256[:8]) digest of the
// realm|space|stack|cell|container tuple, which is deterministic across
// process restarts (so a freshly-spawned `kuke attach` re-derives the same
// path the provisioning runner staged) and uniquely identifies a
// container without inheriting the depth of the metadata layout. With a
// RunPath ≤ 60 bytes the resolved path is `<RunPath>/s/<16hex>` ≤ 79
// bytes — well inside SUN_PATH.
func ContainerSocketSymlinkPath(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerSocketSymlinkDir(baseRunPath),
		containerSocketShortID(realmName, spaceName, stackName, cellName, containerName),
	)
}

// containerSocketShortID returns the 16-hex digest that names the per-
// container symlink inside ContainerSocketSymlinkDir. Derived from
// sha256("<realm>_<space>_<stack>_<cell>_<container>")[:8] so the same
// identity tuple always resolves to the same name across processes. The
// "_" separator is chosen because naming.ValidateRealmName and
// naming.ValidateHierarchyName both reject "_" (and "/") in any
// identifier — every other byte, including "|", is a legal name
// character — so the join is unambiguous: two distinct tuples cannot
// produce the same concatenation regardless of any byte values inside
// the names. 64 bits of randomness puts collisions astronomically out
// of reach for any plausible per-host container count.
func containerSocketShortID(realmName, spaceName, stackName, cellName, containerName string) string {
	digest := sha256.Sum256([]byte(
		realmName + "_" + spaceName + "_" + stackName + "_" + cellName + "_" + containerName,
	))
	return hex.EncodeToString(digest[:8])
}

// ContainerCapturePath returns the host-side path of the per-container sbsh
// capture file. It is the host-visible peer of the in-container path
// /run/kukeon/tty/capture (ctr.AttachableCapturePath); both resolve to the
// same inode because the per-container tty directory is bind-mounted into
// the container. `kuke log` tails this path.
func ContainerCapturePath(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerTTYDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName),
		consts.KukeonContainerCaptureFile,
	)
}

// ContainerKukettyLogPath returns the host-side path of the per-Attachable-
// container kuketty wrapper's own slog output. It is the host-visible peer of
// the in-container ctr.AttachableKukettyLogPath; both resolve to the same
// inode through the per-container tty directory bind mount. The path is
// daemon-controlled (operators do not configure it via cell YAML) so an
// operator debugging a misbehaving attach session always knows where to
// look. Issue #599.
func ContainerKukettyLogPath(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerTTYDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName),
		consts.KukeonContainerKukettyLogFile,
	)
}

// ContainerLogPath returns the host-side path of the per-container stdout/
// stderr log file written by the containerd runtime shim via cio.LogFile.
// Used for non-Attachable containers (kukeond included): the shim opens this
// path and appends every byte the task writes to stdout or stderr; `kuke log`
// tails the same path. Attachable containers do not use this file — their
// output is captured by sbsh into ContainerCapturePath.
func ContainerLogPath(baseRunPath, realmName, spaceName, stackName, cellName, containerName string) string {
	return filepath.Join(
		ContainerMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName, containerName),
		consts.KukeonContainerLogFile,
	)
}

// SecretScopeDir returns the metadata directory of the scope a `kind: Secret`
// is bound to (issue #619). The scope is the deepest non-empty coordinate:
// passing only realmName yields the realm dir, realmName+spaceName the space
// dir, and so on. Empty deeper coordinates are ignored — the caller is
// responsible for having validated coordinate completeness (a non-empty cell
// requires a non-empty stack, space, and realm) before calling.
func SecretScopeDir(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	switch {
	case cellName != "":
		return CellMetadataDir(baseRunPath, realmName, spaceName, stackName, cellName)
	case stackName != "":
		return StackMetadataDir(baseRunPath, realmName, spaceName, stackName)
	case spaceName != "":
		return SpaceMetadataDir(baseRunPath, realmName, spaceName)
	default:
		return RealmMetadataDir(baseRunPath, realmName)
	}
}

// SecretsDir returns the per-scope secrets directory — the root-only
// (0o700) directory under the scope's metadata tree that owns daemon-managed
// secret bytes. Nested inside the scope dir so scope teardown reclaims it.
func SecretsDir(baseRunPath, realmName, spaceName, stackName, cellName string) string {
	return filepath.Join(
		SecretScopeDir(baseRunPath, realmName, spaceName, stackName, cellName),
		consts.KukeonSecretsSubdir,
	)
}

// SecretPath returns the host-side path of a single daemon-managed secret's
// bytes: <scope>/secrets/<name>, root-owned and 0o600.
func SecretPath(baseRunPath, realmName, spaceName, stackName, cellName, secretName string) string {
	return filepath.Join(
		SecretsDir(baseRunPath, realmName, spaceName, stackName, cellName),
		secretName,
	)
}

// BlueprintScopeDir returns the metadata directory of the scope a
// `kind: CellBlueprint` is bound to (issue #620). Unlike a Secret, a Blueprint
// is scopable at realm/space/stack only — never cell — so the deepest
// coordinate this resolves is the stack dir. The caller is responsible for
// having validated coordinate completeness.
func BlueprintScopeDir(baseRunPath, realmName, spaceName, stackName string) string {
	switch {
	case stackName != "":
		return StackMetadataDir(baseRunPath, realmName, spaceName, stackName)
	case spaceName != "":
		return SpaceMetadataDir(baseRunPath, realmName, spaceName)
	default:
		return RealmMetadataDir(baseRunPath, realmName)
	}
}

// BlueprintsDir returns the per-scope blueprints directory — the root-owned,
// world-readable (0o755) directory under the scope's metadata tree that owns
// daemon-managed blueprint documents (issue #620). Nested inside the scope dir
// so scope teardown reclaims it.
func BlueprintsDir(baseRunPath, realmName, spaceName, stackName string) string {
	return filepath.Join(
		BlueprintScopeDir(baseRunPath, realmName, spaceName, stackName),
		consts.KukeonBlueprintsSubdir,
	)
}

// BlueprintPath returns the host-side path of a single daemon-managed
// blueprint document: <scope>/blueprints/<name>, root-owned and 0o644.
func BlueprintPath(baseRunPath, realmName, spaceName, stackName, blueprintName string) string {
	return filepath.Join(
		BlueprintsDir(baseRunPath, realmName, spaceName, stackName),
		blueprintName,
	)
}

// ConfigScopeDir returns the metadata directory of the scope a
// `kind: CellConfig` is bound to (issue #624). Like a CellBlueprint a Config is
// scopable at realm/space/stack only — never cell — so the deepest coordinate
// this resolves is the stack dir. The caller is responsible for having
// validated coordinate completeness.
func ConfigScopeDir(baseRunPath, realmName, spaceName, stackName string) string {
	switch {
	case stackName != "":
		return StackMetadataDir(baseRunPath, realmName, spaceName, stackName)
	case spaceName != "":
		return SpaceMetadataDir(baseRunPath, realmName, spaceName)
	default:
		return RealmMetadataDir(baseRunPath, realmName)
	}
}

// ConfigsDir returns the per-scope configs directory — the root-owned,
// world-readable (0o755) directory under the scope's metadata tree that owns
// daemon-managed config documents (issue #624). Nested inside the scope dir so
// scope teardown reclaims it.
func ConfigsDir(baseRunPath, realmName, spaceName, stackName string) string {
	return filepath.Join(
		ConfigScopeDir(baseRunPath, realmName, spaceName, stackName),
		consts.KukeonConfigsSubdir,
	)
}

// ConfigPath returns the host-side path of a single daemon-managed config
// document: <scope>/configs/<name>, root-owned and 0o644.
func ConfigPath(baseRunPath, realmName, spaceName, stackName, configName string) string {
	return filepath.Join(
		ConfigsDir(baseRunPath, realmName, spaceName, stackName),
		configName,
	)
}

type metadataHeader struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
}

// DetectMetadataVersion detects the API version from raw metadata bytes by parsing the apiVersion field.
// It returns the normalized version using apischeme.DefaultVersion.
func DetectMetadataVersion(raw []byte) (v1beta1.Version, error) {
	var header metadataHeader
	if err := json.Unmarshal(raw, &header); err != nil {
		return "", fmt.Errorf("metadata header: %w", err)
	}
	return apischeme.DefaultVersion(v1beta1.Version(header.APIVersion)), nil
}

// ConvertCellListToExternal converts a slice of internal cells to a slice of external cell docs.
func ConvertCellListToExternal(internalCells []intmodel.Cell) ([]*v1beta1.CellDoc, error) {
	externalCells := make([]*v1beta1.CellDoc, 0, len(internalCells))
	for _, cell := range internalCells {
		cellDoc, convertErr := apischeme.BuildCellExternalFromInternal(cell, apischeme.VersionV1Beta1)
		if convertErr != nil {
			return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
		}
		externalCells = append(externalCells, &cellDoc)
	}
	return externalCells, nil
}

// ConvertStackListToExternal converts a slice of internal stacks to a slice of external stack docs.
func ConvertStackListToExternal(internalStacks []intmodel.Stack) ([]*v1beta1.StackDoc, error) {
	externalStacks := make([]*v1beta1.StackDoc, 0, len(internalStacks))
	for _, stack := range internalStacks {
		stackDoc, convertErr := apischeme.BuildStackExternalFromInternal(stack, apischeme.VersionV1Beta1)
		if convertErr != nil {
			return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
		}
		externalStacks = append(externalStacks, &stackDoc)
	}
	return externalStacks, nil
}

// ConvertSpaceListToExternal converts a slice of internal spaces to a slice of external space docs.
func ConvertSpaceListToExternal(internalSpaces []intmodel.Space) ([]*v1beta1.SpaceDoc, error) {
	externalSpaces := make([]*v1beta1.SpaceDoc, 0, len(internalSpaces))
	for _, space := range internalSpaces {
		spaceDoc, convertErr := apischeme.BuildSpaceExternalFromInternal(space, apischeme.VersionV1Beta1)
		if convertErr != nil {
			return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
		}
		externalSpaces = append(externalSpaces, &spaceDoc)
	}
	return externalSpaces, nil
}

// ConvertRealmListToExternal converts a slice of internal realms to a slice of external realm docs.
func ConvertRealmListToExternal(internalRealms []intmodel.Realm) ([]*v1beta1.RealmDoc, error) {
	externalRealms := make([]*v1beta1.RealmDoc, 0, len(internalRealms))
	for _, realm := range internalRealms {
		realmDoc, convertErr := apischeme.BuildRealmExternalFromInternal(realm, apischeme.VersionV1Beta1)
		if convertErr != nil {
			return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
		}
		externalRealms = append(externalRealms, &realmDoc)
	}
	return externalRealms, nil
}

// ConvertContainerSpecListToExternal converts a slice of internal container specs to a slice of external specs.
func ConvertContainerSpecListToExternal(internalSpecs []intmodel.ContainerSpec) ([]*v1beta1.ContainerSpec, error) {
	externalSpecs := make([]*v1beta1.ContainerSpec, 0, len(internalSpecs))
	for _, spec := range internalSpecs {
		containerSpec := apischeme.BuildContainerSpecExternalFromInternal(spec)
		externalSpecs = append(externalSpecs, &containerSpec)
	}
	return externalSpecs, nil
}

// ConvertCellToExternal converts an internal cell to an external cell doc.
func ConvertCellToExternal(internalCell intmodel.Cell) (*v1beta1.CellDoc, error) {
	cellDoc, convertErr := apischeme.BuildCellExternalFromInternal(internalCell, apischeme.VersionV1Beta1)
	if convertErr != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
	}
	return &cellDoc, nil
}

// ConvertStackToExternal converts an internal stack to an external stack doc.
func ConvertStackToExternal(internalStack intmodel.Stack) (*v1beta1.StackDoc, error) {
	stackDoc, convertErr := apischeme.BuildStackExternalFromInternal(internalStack, apischeme.VersionV1Beta1)
	if convertErr != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
	}
	return &stackDoc, nil
}

// ConvertSpaceToExternal converts an internal space to an external space doc.
func ConvertSpaceToExternal(internalSpace intmodel.Space) (*v1beta1.SpaceDoc, error) {
	spaceDoc, convertErr := apischeme.BuildSpaceExternalFromInternal(internalSpace, apischeme.VersionV1Beta1)
	if convertErr != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
	}
	return &spaceDoc, nil
}

// ConvertRealmToExternal converts an internal realm to an external realm doc.
func ConvertRealmToExternal(internalRealm intmodel.Realm) (*v1beta1.RealmDoc, error) {
	realmDoc, convertErr := apischeme.BuildRealmExternalFromInternal(internalRealm, apischeme.VersionV1Beta1)
	if convertErr != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
	}
	return &realmDoc, nil
}

// ConvertContainerToExternal converts an internal container to an external container doc.
func ConvertContainerToExternal(internalContainer intmodel.Container) (*v1beta1.ContainerDoc, error) {
	containerDoc, convertErr := apischeme.BuildContainerExternalFromInternal(
		internalContainer,
		apischeme.VersionV1Beta1,
	)
	if convertErr != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, convertErr)
	}
	return &containerDoc, nil
}
