// Package installation owns the product orchestration for personal App
// installations: self-service install, the unified installation ACL, and the
// workspace share approval flow. Persistence mechanics live in the
// serviceactor substrate and the store/app/installation child package.
package installation

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// Hooks carry the workspace-wide catalog refresh side effects. The server
// wiring injects the same closures the App store uses.
type Hooks struct {
	AgentCatalogChange func(ctx context.Context, tx coredb.Tx, workspaceID string, agentID string, now time.Time) error
	WorkspaceReconcile func(ctx context.Context, tx coredb.Tx, workspaceID string, now time.Time) error
}

// Service orchestrates personal installations and their ACL.
type Service struct {
	db     coredb.DB
	actors *serviceactor.Store
	hooks  Hooks
}

// New constructs the installation service.
func New(db coredb.DB, actors *serviceactor.Store, hooks Hooks) *Service {
	if actors == nil {
		actors = serviceactor.New(db)
	}
	return &Service{db: db, actors: actors, hooks: hooks}
}

// InstallParams selects an installable App and its configuration.
type InstallParams struct {
	AppID   string
	AppSlug string
	Config  map[string]any
}

// PersonalInstallation is the caller-facing projection of one installation
// usable by the caller.
type PersonalInstallation struct {
	ID                string
	AppID             string
	AppSlug           string
	AppName           string
	AvatarURL         string
	InstanceKey       string
	OwnershipKind     string
	OwnerMemberID     string
	MemberID          string
	Handle            string
	InstalledVer      string
	Config            map[string]any
	RuntimeCapability *registry.AppRuntimeCapabilityView
	Connections       []runtime.ConnectionView
	CanManage         bool
	Enabled           bool
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

// AccessGrantParams grants use or manage access on one installation.
type AccessGrantParams struct {
	PrincipalKind string
	PrincipalRef  string
	AccessRole    string
	Scopes        []string
}

var workspaceAccessAction = access.HumanAction{
	Resource: access.ResourceWorkspace,
	Action:   access.ActionAccess,
}

// InstallForSelf installs one App as the calling member's personal
// installation: the installer becomes the owner with an explicit manage
// authorization, without workspace approval or visibility.
func (svc *Service) InstallForSelf(ctx context.Context, userID string, workspaceID string, params InstallParams) (PersonalInstallation, error) {
	workspaceMember, err := svc.loadAuthorizedMember(ctx, svc.db, userID, workspaceID, false)
	if err != nil {
		return PersonalInstallation{}, err
	}
	memberID := strings.TrimSpace(workspaceMember.Member.ID)

	var installationID string
	err = coredb.WithTx(ctx, svc.db, func(tx coredb.Tx) error {
		// Canonical lock order: resolve only the definition identity, take
		// the definition mutation lock, then reload and validate the app,
		// version, config schema, and feature-owned admission under it so a
		// concurrent definition/version/capability change cannot install
		// against a stale snapshot.
		app, err := installation.LoadWorkspaceInstallableApp(ctx, tx, workspaceID, params.AppID, params.AppSlug)
		if err != nil {
			return err
		}
		if strings.TrimSpace(app.ID) == "app_github" {
			return storeerr.BadRequest("GitHub must be installed and configured through GitHub setup.")
		}
		if err := installation.LockAppInstallationMutationScope(ctx, tx, app.ID); err != nil {
			return err
		}
		currentMember, err := svc.loadAuthorizedMember(ctx, tx, userID, workspaceID, true)
		if err != nil {
			return err
		}
		if strings.TrimSpace(currentMember.Member.ID) != memberID {
			return storeerr.Conflict("installer membership changed.")
		}
		// Locked revalidation: the app row, latest version, schema, and
		// feature-owned admission resolve under the lock.
		app, err = installation.LoadWorkspaceInstallableAppForUpdate(ctx, tx, workspaceID, app.ID, "")
		if err != nil {
			return err
		}
		version, err := installation.LoadInstallableAppVersionForUpdate(ctx, tx, app.ID, "", app.LatestVersion)
		if err != nil {
			return err
		}
		configValue := registry.JSONObjectOrEmpty(params.Config)
		if err := registry.ValidateJSONObjectAgainstManifestSchema("config", version.ConfigSchema, configValue); err != nil {
			return err
		}
		configJSON, err := installation.MarshalJSONObject(configValue, "config")
		if err != nil {
			return err
		}
		if err := installation.RequireGenericInstallAllowedForUpdate(ctx, tx, app.ID); err != nil {
			var featureOwned *installation.FeatureOwnedSetupError
			if errors.As(err, &featureOwned) {
				return storeerr.BadRequest(err.Error())
			}
			return err
		}

		now := time.Now().UTC()
		txStore := svc.actors.InTx(tx)
		installationID, err = installation.InstallPreparedInstance(
			ctx, tx, txStore, workspaceID, memberID,
			installation.PreparedInstance{
				DefinitionID:  app.ID,
				Version:       version.Version,
				VersionScopes: version.Scopes,
				ConfigJSON:    configJSON,
			},
			installation.InstanceIdentity{
				InstanceKey: serviceactor.PersonalInstanceKeyPrefix + memberID,
				Handle:      installation.PersonalHandle(app.Slug, memberID),
				DisplayName: app.Name,
			},
			installation.InstallCallbacks{
				Reconcile: svc.hooks.WorkspaceReconcile,
			},
			now,
		)
		if err != nil {
			return err
		}
		// Ownership is the single authority source for the owner: the
		// owner_member_id column already implies manage, so no redundant
		// owner ACL row is written.
		return nil
	})
	if err != nil {
		return PersonalInstallation{}, err
	}
	return svc.loadPersonalInstallation(ctx, workspaceID, installationID, memberID)
}

func (svc *Service) loadPersonalInstallation(ctx context.Context, workspaceID string, installationID string, viewerMemberID string) (PersonalInstallation, error) {
	item := PersonalInstallation{}
	var configJSON, metadataJSON []byte
	err := svc.db.QueryRow(ctx, `
SELECT si.id, si.service_definition_id, sd.slug, sd.name, sd.avatar_url,
       COALESCE(si.instance_key, ''), COALESCE(si.ownership_kind, 'workspace'), COALESCE(si.owner_member_id, ''), sm.member_id, sm.handle,
       si.config, si.metadata, si.installed_version, si.status = 'active', si.created_at, si.updated_at
FROM service_installations si
JOIN service_definitions sd ON sd.id = si.service_definition_id
JOIN service_members sm ON sm.service_installation_id = si.id
WHERE si.workspace_id = $1 AND si.id = $2 AND si.service_kind = 'app'
LIMIT 1
`, workspaceID, installationID).Scan(
		&item.ID, &item.AppID, &item.AppSlug, &item.AppName, &item.AvatarURL,
		&item.InstanceKey, &item.OwnershipKind, &item.OwnerMemberID, &item.MemberID, &item.Handle,
		&configJSON, &metadataJSON, &item.InstalledVer, &item.Enabled, &item.CreatedAt, &item.UpdatedAt,
	)
	item.Config = decodeConfigMap(configJSON)
	capability, capabilityErr := pinnedRuntimeCapabilityView(metadataJSON)
	if capabilityErr != nil {
		return PersonalInstallation{}, fmt.Errorf("load personal installation: %w", capabilityErr)
	}
	item.RuntimeCapability = capability
	if err != nil {
		return PersonalInstallation{}, fmt.Errorf("load personal installation: %w", err)
	}
	item.CanManage = item.OwnershipKind == serviceactor.OwnershipKindPersonal && item.OwnerMemberID == strings.TrimSpace(viewerMemberID)
	items := []PersonalInstallation{item}
	if err := svc.attachActiveConnections(ctx, items); err != nil {
		return PersonalInstallation{}, err
	}
	return items[0], nil
}

// GrantAccess grants use or manage access on one installation to a human
// member or an agent. Workspace-wide access only comes from share approval.
func (svc *Service) GrantAccess(ctx context.Context, userID string, workspaceID string, installationID string, params AccessGrantParams) (serviceactor.PrincipalAuthorization, error) {
	workspaceMember, summary, err := svc.requireManagement(ctx, userID, workspaceID, installationID)
	if err != nil {
		return serviceactor.PrincipalAuthorization{}, err
	}
	principalKind := strings.TrimSpace(params.PrincipalKind)
	principalRef := strings.TrimSpace(params.PrincipalRef)
	if principalKind == serviceactor.PrincipalKindWorkspace {
		return serviceactor.PrincipalAuthorization{}, storeerr.BadRequest("workspace access requires the share approval flow")
	}
	if principalKind != serviceactor.PrincipalKindHuman {
		return serviceactor.PrincipalAuthorization{}, storeerr.BadRequest("principalKind must be human; agent authorizations use the agent app-authorization surface")
	}
	if principalRef == "" {
		return serviceactor.PrincipalAuthorization{}, storeerr.BadRequest("principalRef is required")
	}
	// Manage is a personal-installment, human-only delegation; agents are
	// always use-scoped, and workspace installations keep legacy authority.
	accessRole := strings.TrimSpace(params.AccessRole)
	if accessRole == "" {
		accessRole = serviceactor.AccessRoleUse
	}
	if accessRole != serviceactor.AccessRoleUse && accessRole != serviceactor.AccessRoleManage {
		return serviceactor.PrincipalAuthorization{}, storeerr.BadRequest("accessRole must be use or manage")
	}
	if accessRole == serviceactor.AccessRoleManage &&
		(principalKind != serviceactor.PrincipalKindHuman || summary.Installation.OwnershipKind != serviceactor.OwnershipKindPersonal) {
		return serviceactor.PrincipalAuthorization{}, storeerr.BadRequest("manage grants require a personal installation and a human principal")
	}

	var authorization serviceactor.PrincipalAuthorization
	err = coredb.WithTx(ctx, svc.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		currentMember, err := svc.revalidateManagement(ctx, tx, userID, workspaceID, workspaceMember.Member.ID, summary)
		if err != nil {
			return err
		}
		if err := access.RequireActiveHumanMember(ctx, tx, workspaceID, principalRef, true); err != nil {
			if errors.Is(err, access.ErrNotFound) {
				return storeerr.BadRequest("principalRef is not a workspace member")
			}
			if errors.Is(err, access.ErrForbidden) {
				return storeerr.BadRequest("principalRef must be an active human member")
			}
			return err
		}
		grants, err := svc.actors.InTx(tx).ListActiveGrantScopesByInstallationIDsForUpdate(ctx, []string{summary.Installation.ID})
		if err != nil {
			return err
		}
		activeScopes := serviceactor.NormalizeScopeSet(grants[summary.Installation.ID])
		scopes := serviceactor.NormalizeScopeSet(params.Scopes)
		if len(scopes) == 0 {
			scopes = activeScopes
		} else if !serviceactor.ScopeSetCovers(activeScopes, scopes) {
			return storeerr.Forbidden("grant_scope_denied")
		}
		var upsertErr error
		authorization, upsertErr = svc.actors.InTx(tx).UpsertPrincipalAuthorization(ctx, serviceactor.PrincipalAuthorizationUpsert{
			WorkspaceID:            workspaceID,
			ServiceDefinitionID:    summary.Definition.ID,
			ServiceInstallationID:  summary.Installation.ID,
			ServiceKind:            serviceactor.KindApp,
			PrincipalKind:          principalKind,
			PrincipalRef:           principalRef,
			AccessRole:             accessRole,
			Scopes:                 scopes,
			AuthorizedByMemberID:   strings.TrimSpace(currentMember.Member.ID),
			Now:                    time.Now().UTC(),
			ActiveAuthorizationKey: serviceactor.PrincipalAuthorizationKeyDigest(workspaceID, principalKind, principalRef, summary.Installation.ID),
		})
		if upsertErr != nil {
			return upsertErr
		}
		return nil
	})
	if err != nil {
		return serviceactor.PrincipalAuthorization{}, err
	}
	return authorization, nil
}

// ListAccess lists the ACL rows of one installation for managers.
func (svc *Service) ListAccess(ctx context.Context, userID string, workspaceID string, installationID string) ([]serviceactor.PrincipalAuthorization, error) {
	if _, _, err := svc.requireManagement(ctx, userID, workspaceID, installationID); err != nil {
		return nil, err
	}
	return svc.actors.ListInstallationPrincipalAuthorizations(ctx, workspaceID, strings.TrimSpace(installationID))
}

// RevokeAccess disables one ACL row; the owner's own access is not revocable.
func (svc *Service) RevokeAccess(ctx context.Context, userID string, workspaceID string, installationID string, authorizationID string) error {
	workspaceMember, summary, err := svc.requireManagement(ctx, userID, workspaceID, installationID)
	if err != nil {
		return err
	}
	authorizationID = strings.TrimSpace(authorizationID)
	if authorizationID == "" {
		return storeerr.BadRequest("authorizationId is required")
	}
	return coredb.WithTx(ctx, svc.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		if _, err := svc.revalidateManagement(ctx, tx, userID, workspaceID, workspaceMember.Member.ID, summary); err != nil {
			return err
		}
		txStore := svc.actors.InTx(tx)
		// Lock the exact tuple by principal identity and verify the locked
		// row is the requested authorization: a replacement row must never
		// be silently revoked by a stale id.
		authorizations, err := txStore.ListInstallationPrincipalAuthorizations(ctx, workspaceID, strings.TrimSpace(installationID))
		if err != nil {
			return err
		}
		var requested *serviceactor.PrincipalAuthorization
		for idx := range authorizations {
			if authorizations[idx].ID == authorizationID && authorizations[idx].Status == serviceactor.StatusActive {
				requested = &authorizations[idx]
				break
			}
		}
		if requested == nil {
			return storeerr.ErrNotFound
		}
		locked, found, err := txStore.LoadActivePrincipalAuthorizationForUpdate(ctx, workspaceID, requested.PrincipalKind, requested.PrincipalRef, summary.Installation.ID)
		if err != nil {
			return err
		}
		if !found || locked.ID != authorizationID {
			return storeerr.ErrNotFound
		}
		target := &locked
		var scopes []string
		if len(target.ScopesJSON) > 0 {
			if err := json.Unmarshal(target.ScopesJSON, &scopes); err != nil {
				return storeerr.Conflict("authorization scopes are corrupt; refuse revocation")
			}
		}
		_, err = txStore.DisablePrincipalAuthorization(ctx, serviceactor.PrincipalAuthorizationDisable{
			WorkspaceID:            workspaceID,
			ServiceDefinitionID:    summary.Definition.ID,
			ServiceInstallationID:  summary.Installation.ID,
			ServiceKind:            serviceactor.KindApp,
			PrincipalKind:          target.PrincipalKind,
			PrincipalRef:           target.PrincipalRef,
			DisabledReason:         "revoked_by_manager",
			Scopes:                 scopes,
			AuthorizedByMemberID:   target.AuthorizedByMemberID,
			Now:                    time.Now().UTC(),
			ActiveAuthorizationKey: serviceactor.PrincipalAuthorizationKeyDigest(workspaceID, target.PrincipalKind, target.PrincipalRef, summary.Installation.ID),
		})
		if err != nil {
			return err
		}
		if target.PrincipalKind == serviceactor.PrincipalKindAgent && svc.hooks.AgentCatalogChange != nil {
			return svc.hooks.AgentCatalogChange(ctx, tx, workspaceID, target.PrincipalRef, time.Now().UTC())
		}
		if target.PrincipalKind == serviceactor.PrincipalKindWorkspace && svc.hooks.WorkspaceReconcile != nil {
			return svc.hooks.WorkspaceReconcile(ctx, tx, workspaceID, time.Now().UTC())
		}
		return nil
	})
}

func decodeConfigMap(raw []byte) map[string]any {
	if len(raw) == 0 {
		return nil
	}
	var decoded map[string]any
	if err := json.Unmarshal(raw, &decoded); err != nil {
		return nil
	}
	return decoded
}

// pinnedRuntimeCapabilityView decodes the installation's pinned runtime
// capability projection from its durable metadata using the canonical
// registry decoder (transports, events, scopes, notifications, schema).
// An empty payload yields nil without error; a corrupt payload fails
// closed.
func pinnedRuntimeCapabilityView(metadataJSON []byte) (*registry.AppRuntimeCapabilityView, error) {
	if len(metadataJSON) == 0 {
		return nil, nil
	}
	var document struct {
		Manifest json.RawMessage `json:"manifest"`
	}
	if err := json.Unmarshal(metadataJSON, &document); err != nil {
		return nil, fmt.Errorf("decode installation metadata: %w", err)
	}
	if len(document.Manifest) == 0 {
		return nil, nil
	}
	return registry.RuntimeCapabilityViewFromManifest(document.Manifest)
}

func (svc *Service) attachActiveConnections(ctx context.Context, installations []PersonalInstallation) error {
	if len(installations) == 0 {
		return nil
	}
	ids := make([]string, 0, len(installations))
	for idx := range installations {
		ids = append(ids, installations[idx].ID)
		installations[idx].Connections = []runtime.ConnectionView{}
	}
	byInstallation, err := svc.actors.ListActiveConnectionsByInstallationIDs(ctx, ids)
	if err != nil {
		return err
	}
	for idx := range installations {
		connections := byInstallation[installations[idx].ID]
		views := make([]runtime.ConnectionView, 0, len(connections))
		for _, connection := range connections {
			views = append(views, runtime.ConnectionView{
				ID:                 connection.ID,
				ExternalSubjectRef: connection.ExternalSubjectRef,
				DisplayLabel:       connection.DisplayLabel,
				Enabled:            connection.Status == serviceactor.StatusActive,
				CreatedAt:          connection.CreatedAt,
				UpdatedAt:          connection.UpdatedAt,
			})
		}
		installations[idx].Connections = views
	}
	return nil
}

// resolveCanManage marks each installation the viewer may manage. The
// personal owner is resolved in memory; every other installation goes
// through one batched principal-authorization query that fails closed.
func (svc *Service) resolveCanManage(ctx context.Context, workspaceID string, memberID string, installations []PersonalInstallation) error {
	ownedOrLegacy := make(map[int]struct{}, len(installations))
	candidates := make([]string, 0, len(installations))
	for idx := range installations {
		item := &installations[idx]
		if item.OwnershipKind != serviceactor.OwnershipKindPersonal {
			// Workspace installations never resolve through the personal
			// principal grant table; they always take the legacy authority.
			continue
		}
		if item.OwnerMemberID == memberID {
			item.CanManage = true
			ownedOrLegacy[idx] = struct{}{}
			continue
		}
		if item.ID != "" {
			candidates = append(candidates, item.ID)
		}
	}
	if len(candidates) == 0 {
		return nil
	}
	grants, err := svc.actors.ResolveHumanInstallationsAccess(ctx, workspaceID, memberID, candidates)
	if err != nil {
		return err
	}
	byInstallation := make(map[string]string, len(grants))
	for _, grant := range grants {
		current, exists := byInstallation[grant.ServiceInstallationID]
		if !exists || accessRoleRank(grant.AccessRole) > accessRoleRank(current) {
			byInstallation[grant.ServiceInstallationID] = grant.AccessRole
		}
	}
	for idx := range installations {
		if _, done := ownedOrLegacy[idx]; done {
			continue
		}
		if byInstallation[installations[idx].ID] == serviceactor.AccessRoleManage {
			installations[idx].CanManage = true
		}
	}
	return nil
}

func accessRoleRank(role string) int {
	switch strings.TrimSpace(role) {
	case serviceactor.AccessRoleManage:
		return 2
	case serviceactor.AccessRoleUse:
		return 1
	default:
		return 0
	}
}

func (svc *Service) resolveLegacyWorkspaceCanManage(
	ctx context.Context,
	workspaceID string,
	workspaceMember access.HumanWorkspaceMember,
	installations []PersonalInstallation,
) error {
	definitionIDs := make([]string, 0, len(installations))
	for idx := range installations {
		if installations[idx].OwnershipKind == serviceactor.OwnershipKindPersonal {
			continue
		}
		if installations[idx].AppID != "" {
			definitionIDs = append(definitionIDs, installations[idx].AppID)
		}
	}
	if len(definitionIDs) == 0 {
		return nil
	}
	authorities, found, err := installation.LoadDefinitionManagementAuthorities(ctx, svc.db, workspaceID, workspaceMember.Member.ID, definitionIDs)
	if err != nil {
		return err
	}
	if !found {
		return nil
	}
	isAdmin := hasWorkspaceAdminRole(workspaceMember)
	canInstallService := svc.canInstallService(ctx, workspaceMember, workspaceID)
	for idx := range installations {
		if installations[idx].OwnershipKind == serviceactor.OwnershipKindPersonal {
			continue
		}
		authority, ok := authorities[installations[idx].AppID]
		if !ok {
			continue
		}
		if !authority.Attributed {
			if canInstallService {
				installations[idx].CanManage = true
			}
			continue
		}
		if isAdmin || authority.ActiveManager {
			installations[idx].CanManage = true
		}
	}
	return nil
}

// canInstallService reports the workspace service-installer permission for
// the unattributed workspace installation authority.
func (svc *Service) canInstallService(ctx context.Context, workspaceMember access.HumanWorkspaceMember, workspaceID string) bool {
	return access.AuthorizeHumanAction(workspaceMember, workspaceInstallServiceAction) == nil
}
