package app

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// AgentAppAuthorizationView is the browser-facing row for one workspace app in
// an agent's Apps / Integrations settings section.
type AgentAppAuthorizationView struct {
	AppID                string
	AppSlug              string
	AppName              string
	AvatarURL            string
	AppInstallationID    string
	AppMemberID          string
	Handle               string
	InstallationEnabled  bool
	Supported            bool
	MCPName              string
	MCPTools             []string
	Scopes               []string
	AuthorizationID      string
	AuthorizationStatus  string
	DisabledReason       string
	AuthorizedByMemberID string
	PolicyVersion        int64
	AuthorizedAt         *time.Time
	UpdatedAt            *time.Time
	hasEnabledConnection bool
	authorizationRemoved bool
}

// ListAgentAppAuthorizationsForUser returns all workspace app installations
// with the selected agent's current authorization state. The caller must manage
// the agent because enabling an app changes the next runtime tool catalog.
// Other members' personal installations are invisible unless the caller holds
// manage authority over them.
func (store *Store) ListAgentAppAuthorizationsForUser(ctx context.Context, userID string, workspaceID string, agentID string) ([]AgentAppAuthorizationView, error) {
	agentAccess, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID)
	if err != nil {
		return nil, err
	}
	return store.listAgentAppAuthorizationViews(
		ctx,
		workspaceID,
		agentID,
		"",
		strings.TrimSpace(agentAccess.WorkspaceMember.Member.ID),
	)
}

// EnableAgentAppAuthorizationForUser authorizes one installed app for an agent.
func (store *Store) EnableAgentAppAuthorizationForUser(ctx context.Context, userID string, workspaceID string, agentID string, appInstallationID string) (AgentAppAuthorizationView, error) {
	agentAccess, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID)
	if err != nil {
		return AgentAppAuthorizationView{}, err
	}
	// Authorizing an agent onto a personal installation requires manage
	// authority over that installation; agent management alone is not enough.
	if err := store.requireInstallationManageIfPersonal(ctx, userID, workspaceID, appInstallationID); err != nil {
		return AgentAppAuthorizationView{}, err
	}
	installation, found, err := store.loadAgentAuthorizationInstallationView(ctx, workspaceID, appInstallationID)
	if err != nil {
		return AgentAppAuthorizationView{}, err
	}
	if !found || !installation.InstallationEnabled {
		return AgentAppAuthorizationView{}, storeerr.BadRequest("app installation is not enabled")
	}
	if !installation.Capability.Supported {
		return AgentAppAuthorizationView{}, storeerr.BadRequest("app does not expose an agent MCP")
	}
	if _, err := store.UpsertAgentAppAuthorization(ctx, UpsertAgentAppAuthorizationParams{
		WorkspaceID:          workspaceID,
		AgentID:              agentID,
		AppInstallationID:    appInstallationID,
		AuthorizedByMemberID: agentAccess.WorkspaceMember.Member.ID,
		Scopes:               installation.Capability.Scopes,
		Metadata: map[string]any{
			"source": "agent_settings",
		},
		OperatorUserID: userID,
	}); err != nil {
		return AgentAppAuthorizationView{}, err
	}
	return store.getAgentAppAuthorizationView(ctx, workspaceID, agentID, appInstallationID)
}

// DisableAgentAppAuthorizationForUser disables one agent/app authorization.
func (store *Store) DisableAgentAppAuthorizationForUser(ctx context.Context, userID string, workspaceID string, agentID string, appInstallationID string) (AgentAppAuthorizationView, error) {
	if _, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID); err != nil {
		return AgentAppAuthorizationView{}, err
	}
	if _, err := store.DisableAgentAppAuthorization(ctx, workspaceID, agentID, appInstallationID, "disabled_by_owner"); err != nil {
		// A not-found here means there is nothing this agent is authorized
		// for on that installation (including foreign personal installs):
		// fail closed instead of projecting the installation back.
		return AgentAppAuthorizationView{}, err
	}
	return store.getAgentAppAuthorizationView(ctx, workspaceID, agentID, appInstallationID)
}

func (store *Store) getAgentAppAuthorizationView(ctx context.Context, workspaceID string, agentID string, appInstallationID string) (AgentAppAuthorizationView, error) {
	views, err := store.listAgentAppAuthorizationViews(ctx, workspaceID, agentID, appInstallationID, "")
	if err != nil {
		return AgentAppAuthorizationView{}, err
	}
	if len(views) == 0 {
		return AgentAppAuthorizationView{}, storeerr.ErrNotFound
	}
	return views[0], nil
}

func (store *Store) listAgentAppAuthorizationViews(ctx context.Context, workspaceID string, agentID string, appInstallationID string, viewerMemberID string) ([]AgentAppAuthorizationView, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	if strings.TrimSpace(appInstallationID) != "" {
		if err := store.ensureServiceActorAppInstallationFresh(ctx, trimmedWorkspaceID, strings.TrimSpace(appInstallationID)); err != nil {
			return nil, err
		}
	} else {
		if err := store.ensureServiceActorAppWorkspaceFresh(ctx, trimmedWorkspaceID); err != nil {
			return nil, err
		}
	}
	conditions := []string{"si.workspace_id = $1"}
	args := []any{strings.TrimSpace(workspaceID)}
	if strings.TrimSpace(appInstallationID) != "" {
		args = append(args, strings.TrimSpace(appInstallationID))
		conditions = append(conditions, fmt.Sprintf("si.id = $%d", len(args)))
	}
	if strings.TrimSpace(viewerMemberID) != "" {
		args = append(args, strings.TrimSpace(viewerMemberID))
		viewerArg := len(args)
		conditions = append(conditions, fmt.Sprintf(`(
			si.ownership_kind IS NULL
			OR si.ownership_kind <> 'personal'
			OR si.owner_member_id = $%d
			OR EXISTS (
				SELECT 1
				FROM service_principal_authorizations viewer_spa
				WHERE viewer_spa.workspace_id = si.workspace_id
				  AND viewer_spa.service_installation_id = si.id
				  AND viewer_spa.status = 'active'
				  AND viewer_spa.removed_at IS NULL
				  AND viewer_spa.access_role = 'manage'
				  AND (
				    (viewer_spa.principal_kind = 'human' AND viewer_spa.principal_ref = $%d)
				    OR (viewer_spa.principal_kind = 'workspace' AND viewer_spa.principal_ref = si.workspace_id)
				  )
			)
		)`, viewerArg, viewerArg))
	}
	args = append(args, strings.TrimSpace(agentID))
	agentArg := len(args)
	query := fmt.Sprintf(`
SELECT sd.id, sd.slug, sd.name, sd.avatar_url, si.id, sm.member_id, sm.handle, si.status = 'active',
       EXISTS (
           SELECT 1
           FROM service_connections sc
           WHERE sc.service_installation_id = si.id
             AND sc.status = 'active'
       ) AS has_enabled_connection,
       COALESCE(mcp.default_enabled, FALSE), COALESCE(subscription_manage.default_enabled, FALSE),
       COALESCE(mcp.config, '{}'),
       COALESCE(spa.id, ''), COALESCE(spa.status, ''), COALESCE(spa.disabled_reason, ''),
       COALESCE(spa.scopes_json, '[]'), COALESCE(spa.authorized_by_member_id, ''),
       COALESCE(spa.policy_version, 0), spa.created_at, spa.updated_at, spa.removed_at
FROM service_installations si
JOIN service_definitions sd ON sd.id = si.service_definition_id AND sd.service_kind = si.service_kind
JOIN service_members sm ON sm.service_installation_id = si.id
	LEFT JOIN service_capabilities mcp
	  ON mcp.service_definition_id = sd.id
	 AND mcp.capability_key = 'mcp.tools'
	LEFT JOIN service_capabilities subscription_manage
	  ON subscription_manage.service_definition_id = sd.id
	 AND subscription_manage.capability_key = 'subscriptions.manage'
	LEFT JOIN service_principal_authorizations spa
	  ON spa.workspace_id = si.workspace_id
	 AND spa.principal_kind = 'agent'
	 AND spa.principal_ref = $%d
	 AND spa.service_installation_id = si.id
		WHERE %s
		  AND si.service_kind = 'app'
		ORDER BY sd.slug ASC, si.id ASC
	`, agentArg, strings.Join(conditions, " AND "))
	rows, err := store.db.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("list agent app authorization views: %w", err)
	}
	defer rows.Close()

	views := make([]AgentAppAuthorizationView, 0)
	for rows.Next() {
		view, err := scanAgentAppAuthorizationView(rows)
		if err != nil {
			return nil, err
		}
		if !registry.IsProductApp(view.AppID, view.AppSlug) ||
			!view.Supported {
			continue
		}
		views = append(views, view)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate agent app authorization views: %w", err)
	}
	return views, nil
}

func scanAgentAppAuthorizationView(row coredb.Row) (AgentAppAuthorizationView, error) {
	var view AgentAppAuthorizationView
	var scopesJSON []byte
	var hasMCPTools, hasSubscriptions bool
	var mcpConfigJSON string
	var authorizedAt, updatedAt, removedAt sql.NullTime
	if err := row.Scan(
		&view.AppID,
		&view.AppSlug,
		&view.AppName,
		&view.AvatarURL,
		&view.AppInstallationID,
		&view.AppMemberID,
		&view.Handle,
		&view.InstallationEnabled,
		&view.hasEnabledConnection,
		&hasMCPTools,
		&hasSubscriptions,
		&mcpConfigJSON,
		&view.AuthorizationID,
		&view.AuthorizationStatus,
		&view.DisabledReason,
		&scopesJSON,
		&view.AuthorizedByMemberID,
		&view.PolicyVersion,
		&authorizedAt,
		&updatedAt,
		&removedAt,
	); err != nil {
		return AgentAppAuthorizationView{}, err
	}
	view.authorizationRemoved = removedAt.Valid
	capability := agentAppCapabilitySetForService(view.AppSlug, hasMCPTools, hasSubscriptions, []byte(mcpConfigJSON))
	view.Scopes = decodeAgentAppAuthorizationScopes(scopesJSON)
	if len(view.Scopes) == 0 {
		view.Scopes = capability.Scopes
	}
	view.MCPTools = capability.Tools
	view.Supported = capability.Supported
	if view.Supported {
		view.MCPName = capability.MCPName
	}
	authorizationStatus := strings.ToLower(strings.TrimSpace(view.AuthorizationStatus))
	switch {
	case view.authorizationRemoved:
		view.AuthorizationStatus = AgentAppAuthorizationStatusDisabled
		if strings.TrimSpace(view.DisabledReason) == "" {
			view.DisabledReason = "app_authorization_removed"
		}
	case view.Supported && !view.InstallationEnabled:
		view.AuthorizationStatus = AgentAppAuthorizationStatusDisabled
		if strings.TrimSpace(view.DisabledReason) == "" || authorizationStatus == AgentAppAuthorizationStatusActive {
			view.DisabledReason = "app_installation_disabled"
		}
	case view.Supported && !view.hasEnabledConnection:
		view.AuthorizationStatus = AgentAppAuthorizationStatusDisabled
		if strings.TrimSpace(view.DisabledReason) == "" || authorizationStatus == AgentAppAuthorizationStatusActive {
			view.DisabledReason = "app_connection_not_configured"
		}
	case view.AuthorizationStatus == "":
		if view.InstallationEnabled &&
			view.Supported &&
			view.hasEnabledConnection &&
			!agentAppAuthorizationRequiresExplicit(capability) {
			view.AuthorizationStatus = AgentAppAuthorizationStatusActive
			view.PolicyVersion = 1
		} else {
			view.AuthorizationStatus = AgentAppAuthorizationStatusDisabled
		}
	}
	if authorizedAt.Valid {
		view.AuthorizedAt = &authorizedAt.Time
	}
	if updatedAt.Valid {
		view.UpdatedAt = &updatedAt.Time
	}
	return view, nil
}

type agentAuthorizationInstallationView struct {
	AppID               string
	AppSlug             string
	InstallationID      string
	InstallationEnabled bool
	Capability          agentAppCapabilitySet
}

func (store *Store) loadAgentAuthorizationInstallationView(ctx context.Context, workspaceID string, appInstallationID string) (agentAuthorizationInstallationView, bool, error) {
	if err := store.ensureServiceActorAppInstallationFresh(ctx, workspaceID, appInstallationID); err != nil {
		return agentAuthorizationInstallationView{}, false, err
	}
	row := store.db.QueryRow(ctx, `
SELECT si.service_definition_id, sd.slug, si.id, si.status = 'active',
       COALESCE(mcp.default_enabled, FALSE), COALESCE(subscription_manage.default_enabled, FALSE),
       COALESCE(mcp.config, '{}')
FROM service_installations si
JOIN service_definitions sd
  ON sd.id = si.service_definition_id
 AND sd.service_kind = si.service_kind
LEFT JOIN service_capabilities mcp
  ON mcp.service_definition_id = sd.id
 AND mcp.capability_key = 'mcp.tools'
LEFT JOIN service_capabilities subscription_manage
  ON subscription_manage.service_definition_id = sd.id
 AND subscription_manage.capability_key = 'subscriptions.manage'
WHERE si.workspace_id = $1
  AND si.id = $2
  AND si.service_kind = 'app'
`, strings.TrimSpace(workspaceID), strings.TrimSpace(appInstallationID))
	var view agentAuthorizationInstallationView
	var hasMCPTools, hasSubscriptions bool
	var mcpConfigJSON string
	if err := row.Scan(&view.AppID, &view.AppSlug, &view.InstallationID, &view.InstallationEnabled, &hasMCPTools, &hasSubscriptions, &mcpConfigJSON); err != nil {
		if coredb.IsNotFound(err) {
			return agentAuthorizationInstallationView{}, false, nil
		}
		return agentAuthorizationInstallationView{}, false, err
	}
	view.Capability = agentAppCapabilitySetForService(view.AppSlug, hasMCPTools, hasSubscriptions, []byte(mcpConfigJSON))
	return view, true, nil
}

func agentAppAuthorizationRequiresExplicit(
	capability agentAppCapabilitySet,
) bool {
	return capability.AuthorizationMode ==
		model.AppAuthorizationModeExplicitOnly
}
