// Package catalogread exposes Catalog-owned, request-serving read projections.
//
// Reader is intentionally narrower than catalogapp.Authority. It owns no
// Catalog lifecycle transition, release registration, binding write, revision
// allocation, cache, or fallback. Consumers read the same authoritative
// Catalog rows through this package so a serving process does not need to link
// the Catalog writer state machine merely to verify its selected installation
// and workload binding.
package catalogread

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

var ErrNotFound = errors.New("catalog serving read: not found")

// Reader is the Catalog-owned read facade for independently serving consumers.
// It has no exported database handle and exposes no mutation operation.
type Reader struct {
	db *sql.DB
}

// New creates a Catalog read facade over the Catalog authority database.
func New(db *sql.DB) (*Reader, error) {
	if db == nil {
		return nil, errors.New("catalog serving read: database is required")
	}
	return &Reader{db: db}, nil
}

// ServingInstallation is the minimal Catalog projection Runtime needs to
// verify one selected AppInstallation. It intentionally excludes candidate
// releases, lifecycle commands, permissions, and every authoring input.
type ServingInstallation struct {
	OrgID               string
	AppInstallationID   string
	ActiveReleaseDigest string
	CatalogRevision     uint64
}

// ServingHandlerBinding is the minimal Catalog projection Runtime needs to
// verify the active workload owner for one selected capability.
type ServingHandlerBinding struct {
	OrgID                      string
	AppInstallationID          string
	AppReleaseDigest           string
	CapabilityID               string
	WorkloadID                 string
	ServiceReleaseDigest       string
	OwnershipEpoch             uint64
	CompatibilityReceiptDigest string
}

// ServingCapability is one statement-snapshot of the installation and its
// selected active capability binding. Runtime must use this tuple rather than
// independently assembling facts that could straddle a Catalog activation.
type ServingCapability struct {
	Installation ServingInstallation
	Binding      ServingHandlerBinding
}

// ReadServingCapability returns one complete active installation/capability
// owner tuple in one database statement. Catalog activation commits both
// tables together, so one statement snapshot observes either the state before
// that commit or the state after it, never a synthetic cross-commit tuple. A
// successful result is always complete and internally consistent. A missing or
// non-active installation, or an installation with no active binding for the
// requested capability, returns ErrNotFound; callers that need lifecycle
// diagnostics must use the Catalog lifecycle authority, not this serving view.
func (r *Reader) ReadServingCapability(ctx context.Context, installationID, capabilityID string) (ServingCapability, error) {
	var result ServingCapability
	var rawCatalogRevision []byte
	var activeRelease sql.NullString
	var state string
	var bindingOrgID, bindingInstallationID, bindingAppRelease, bindingCapabilityID sql.NullString
	var bindingWorkloadID, bindingServiceRelease, bindingOwnershipEpoch, bindingReceipt sql.NullString
	err := r.db.QueryRowContext(ctx, `SELECT i.OrgId, i.AppInstallationId, i.ActiveReleaseDigest, i.CatalogRevision, i.State,
b.OrgId, b.AppInstallationId, b.AppReleaseDigest, b.CapabilityId, b.WorkloadId, b.ServiceReleaseDigest,
b.OwnershipEpoch, b.CompatibilityReceiptDigest
FROM _System_CatalogAppInstallation AS i
LEFT JOIN _System_CatalogHandlerBinding AS b
ON b.AppInstallationId = i.AppInstallationId AND b.CapabilityId = ? AND b.State = 'ACTIVE' AND b.ActiveKey IS NOT NULL
WHERE i.AppInstallationId = ?`, capabilityID, installationID).Scan(
		&result.Installation.OrgID, &result.Installation.AppInstallationID, &activeRelease, &rawCatalogRevision, &state,
		&bindingOrgID, &bindingInstallationID, &bindingAppRelease, &bindingCapabilityID, &bindingWorkloadID, &bindingServiceRelease,
		&bindingOwnershipEpoch, &bindingReceipt)
	if errors.Is(err, sql.ErrNoRows) {
		return ServingCapability{}, ErrNotFound
	}
	if err != nil {
		return ServingCapability{}, err
	}
	result.Installation.CatalogRevision, err = parseCatalogRevision(rawCatalogRevision)
	if err != nil {
		return ServingCapability{}, err
	}
	if state != "ACTIVE" {
		return ServingCapability{}, ErrNotFound
	}
	if !present(result.Installation.OrgID) || !present(result.Installation.AppInstallationID) ||
		!activeRelease.Valid || !present(activeRelease.String) {
		return ServingCapability{}, errors.New("catalog serving read: active installation is incomplete")
	}
	result.Installation.ActiveReleaseDigest = activeRelease.String
	if !bindingInstallationID.Valid {
		return ServingCapability{}, ErrNotFound
	}
	if !present(bindingOrgID.String) || !present(bindingInstallationID.String) || !present(bindingAppRelease.String) ||
		!present(bindingCapabilityID.String) || !present(bindingWorkloadID.String) || !present(bindingServiceRelease.String) ||
		!present(bindingOwnershipEpoch.String) || !present(bindingReceipt.String) {
		return ServingCapability{}, errors.New("catalog serving read: active binding is incomplete")
	}
	result.Binding = ServingHandlerBinding{
		OrgID: bindingOrgID.String, AppInstallationID: bindingInstallationID.String, AppReleaseDigest: bindingAppRelease.String,
		CapabilityID: bindingCapabilityID.String, WorkloadID: bindingWorkloadID.String,
		ServiceReleaseDigest: bindingServiceRelease.String, CompatibilityReceiptDigest: bindingReceipt.String,
	}
	result.Binding.OwnershipEpoch, err = parseCatalogRevision([]byte(bindingOwnershipEpoch.String))
	if err != nil {
		return ServingCapability{}, err
	}
	if result.Binding.OwnershipEpoch == 0 || result.Binding.OrgID != result.Installation.OrgID ||
		result.Binding.AppInstallationID != result.Installation.AppInstallationID ||
		result.Binding.AppReleaseDigest != result.Installation.ActiveReleaseDigest ||
		result.Binding.CapabilityID != capabilityID {
		return ServingCapability{}, errors.New("catalog serving read: active binding does not match installation")
	}
	return result, nil
}

func present(value string) bool {
	return strings.TrimSpace(value) != ""
}

func parseCatalogRevision(raw []byte) (uint64, error) {
	value, err := strconv.ParseUint(string(raw), 10, 64)
	if err != nil {
		return 0, fmt.Errorf("catalog serving read: invalid revision: %w", err)
	}
	return value, nil
}
