// Package store owns store initialization and composition wiring.
package store

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/core/app/capabilities"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/admin"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/channel"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/im"
	"github.com/tidbcloud/AgentTeam/internal/store/knowledgeqa"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/onboarding"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/store/privateagent"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
	"github.com/tidbcloud/AgentTeam/internal/store/reaction"
	"github.com/tidbcloud/AgentTeam/internal/store/reporting"
	"github.com/tidbcloud/AgentTeam/internal/store/review"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
	"github.com/tidbcloud/AgentTeam/internal/store/seatbilling"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/team"
	"github.com/tidbcloud/AgentTeam/internal/store/unread"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// Options contains process-level store wiring inputs.
//
// Keep this package independent from API handlers: callers should decompose
// Graph into narrow ports at composition roots and pass only those ports onward.
type Options struct {
	AppCredentialStorageKey           []byte
	LegacyAppCredentialStorageKeys    [][]byte
	Logger                            *slog.Logger
	HTTPClient                        *http.Client
	PublicBaseURL                     string
	GitHubAPIBaseURL                  string
	MediaPublicBaseURL                string
	MediaObjectPrefix                 string
	CheckWorkspaceQuotaInTx           func(context.Context, coredb.Tx, string, plan.MetricKey, int64) error
	CheckWorkspaceAgentOwnerQuotaInTx func(context.Context, coredb.Tx, string, string, int64) error
	AppNotificationInternalServices   []app.AppNotificationInternalServiceRegistration
}

// Graph holds the store dependency graph for composition roots.
//
// Graph intentionally has no methods and no business workflow. It is a named
// composition container so server/e2e wiring can construct stores in one place
// while API handlers receive narrow dependencies.
type Graph struct {
	App            *app.Store
	Approval       *approval.Store
	Provider       *provider.Store
	Identity       *identity.Store
	Workspace      *workspace.Store
	IM             *im.Store
	KnowledgeQA    *knowledgeqa.Store
	Channel        *channel.Store
	Plan           *plan.Store
	SeatBilling    *seatbilling.Store
	Admin          *admin.Store
	Agent          *agent.Service
	AgentOps       *agentops.Store
	PrivateAgents  *privateagent.Store
	Attachment     *attachment.Store
	Reaction       *reaction.Store
	Message        *message.Store
	Onboarding     *onboarding.Store
	Rewind         *rewind.Store
	Review         *review.Store
	Reporting      *reporting.Store
	Team           *team.Store
	ServiceAccount *serviceaccount.Store
	ServiceActor   *serviceactor.Store
	UnreadRuntime  *unread.Store
}

// New constructs the store graph with default options.
func New(db coredb.DB, archiveStorage archive.Storage, mediaStorage objectstorage.Storage, hotMessageRetentionDays int) *Graph {
	return NewWithOptions(db, archiveStorage, mediaStorage, hotMessageRetentionDays, Options{})
}

func cloneCredentialStorageKeys(keys [][]byte) [][]byte {
	if len(keys) == 0 {
		return nil
	}
	cloned := make([][]byte, 0, len(keys))
	for _, key := range keys {
		if len(key) == 0 {
			continue
		}
		cloned = append(cloned, append([]byte(nil), key...))
	}
	return cloned
}

// NewWithOptions constructs the store graph from process-level inputs.
func NewWithOptions(db coredb.DB, archiveStorage archive.Storage, mediaStorage objectstorage.Storage, hotMessageRetentionDays int, options Options) *Graph {
	signer, err := capabilities.NewEphemeralSigner()
	if err != nil {
		panic(fmt.Sprintf("initialize app capability signer: %v", err))
	}

	githubAPIBaseURL := strings.TrimRight(strings.TrimSpace(options.GitHubAPIBaseURL), "/")
	if githubAPIBaseURL == "" {
		githubAPIBaseURL = "https://api.github.com"
	}
	graph := &Graph{}
	graph.Plan = plan.New(db)
	graph.SeatBilling = seatbilling.New(db)
	checkWorkspaceQuotaInTx := graph.Plan.CheckWorkspaceQuotaInTx
	if options.CheckWorkspaceQuotaInTx != nil {
		checkWorkspaceQuotaInTx = options.CheckWorkspaceQuotaInTx
	}
	checkWorkspaceAgentOwnerQuotaInTx := graph.Plan.CheckWorkspaceAgentOwnerQuotaInTx
	if options.CheckWorkspaceAgentOwnerQuotaInTx != nil {
		checkWorkspaceAgentOwnerQuotaInTx = options.CheckWorkspaceAgentOwnerQuotaInTx
	}
	graph.App = app.New(db, app.Options{
		Logger:                          options.Logger,
		AppCapabilitySigner:             signer,
		AppCredentialStorageKey:         append([]byte(nil), options.AppCredentialStorageKey...),
		LegacyAppCredentialStorageKeys:  cloneCredentialStorageKeys(options.LegacyAppCredentialStorageKeys),
		HTTPClient:                      options.HTTPClient,
		PublicBaseURL:                   options.PublicBaseURL,
		GitHubAPIBaseURL:                githubAPIBaseURL,
		ArchiveStorage:                  archiveStorage,
		MediaObjectStorage:              mediaStorage,
		MediaPublicBaseURL:              options.MediaPublicBaseURL,
		MediaObjectPrefix:               options.MediaObjectPrefix,
		HotMessageRetentionDays:         hotMessageRetentionDays,
		AppNotificationInternalServices: options.AppNotificationInternalServices,
		RequireWorkspaceAccess: func(ctx context.Context, userID string, workspaceID string) error {
			_, err := access.LoadAuthorizedHumanWorkspaceMember(ctx, db, userID, workspaceID, access.HumanAction{Resource: access.ResourceWorkspace, Action: access.ActionAccess})
			return mapAccessError(err)
		},
		CheckWorkspaceQuotaInTx:           checkWorkspaceQuotaInTx,
		CheckWorkspaceAgentOwnerQuotaInTx: checkWorkspaceAgentOwnerQuotaInTx,
	})
	graph.Approval = approval.New(db)
	graph.KnowledgeQA = knowledgeqa.New(db)
	graph.Provider = graph.App.Provider()
	graph.ServiceAccount = serviceaccount.New(db)
	graph.ServiceActor = serviceactor.New(db)
	graph.Team = team.New(db)
	graph.UnreadRuntime = unread.New(db)
	graph.Identity = identity.New(db, identity.Options{
		RequireSystemAdminUser: func(ctx context.Context, userID string) error {
			return mapAccessError(access.RequireSystemAdminUser(ctx, db, userID))
		},
	})
	graph.Onboarding = onboarding.New(db)
	graph.AgentOps = agentops.New(db, agentops.Options{Logger: options.Logger, OnboardingEvidence: graph.Onboarding})
	graph.Admin = admin.New(db, admin.Options{RequireSystemAdminUser: func(ctx context.Context, userID string) error {
		return mapAccessError(access.RequireSystemAdminUser(ctx, db, userID))
	}})
	graph.Reporting = reporting.New(db, mediaStorage, reporting.Options{})
	graph.Workspace = workspace.New(db, workspace.Options{
		RequireWorkspaceAccess: func(ctx context.Context, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
			member, err := access.LoadAuthorizedHumanWorkspaceMember(ctx, db, userID, workspaceID, access.HumanAction{Resource: access.ResourceWorkspace, Action: access.ActionAccess})
			return member, mapAccessError(err)
		},
		RequireWorkspaceMemberManagement: func(ctx context.Context, userID string, workspaceID string) error {
			_, err := access.LoadAuthorizedHumanWorkspaceMember(ctx, db, userID, workspaceID, access.HumanAction{Resource: access.ResourceWorkspace, Action: access.ActionManageMembers})
			return mapAccessError(err)
		},
		EnsureAllChannelMembershipInTx: func(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string) error {
			if graph.PrivateAgents == nil {
				return fmt.Errorf("private-agent projection store is not configured")
			}
			return graph.PrivateAgents.EnsureAllChannelScopeParticipantProjection(ctx, tx, workspaceID, memberID, "no-wake", time.Now().UTC())
		},
		EnsureServiceAccountAudienceMembershipsInTx: func(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string) error {
			if graph.ServiceAccount == nil {
				return fmt.Errorf("service-account store is not configured")
			}
			return graph.ServiceAccount.EnsureHumanAudienceMembershipsInTx(ctx, tx, workspaceID, memberID, time.Now().UTC())
		},
		CheckWorkspaceQuotaInTx: checkWorkspaceQuotaInTx,
	})
	graph.Attachment = attachment.New(db, archiveStorage, mediaStorage, hotMessageRetentionDays, graph.App.AttachmentOptions())
	graph.IM = im.New(db)
	graph.Message = message.New(db, archiveStorage, hotMessageRetentionDays, message.Options{
		LoadAttachmentsForMessages:  graph.Attachment.LoadAttachmentsForMessages,
		DeleteAttachmentDerivatives: graph.Attachment.DeleteAttachmentThumbnail,
	})
	graph.PrivateAgents = privateagent.New(db, graph.App.PrivateAgentProjectionOptions())
	graph.Channel = channel.New(db, channel.Options{
		RequireWorkspaceAccess: func(ctx context.Context, userID string, workspaceID string) (string, error) {
			member, err := access.LoadAuthorizedHumanWorkspaceMember(ctx, db, userID, workspaceID, access.HumanAction{Resource: access.ResourceWorkspace, Action: access.ActionAccess})
			if err != nil {
				return "", mapAccessError(err)
			}
			return member.Member.ID, nil
		},
		RequireChannelReadAccess: func(ctx context.Context, userID string, workspaceID string, channelID string) (channel.HumanChannelAccess, error) {
			authorized, err := access.LoadAuthorizedHumanChannelAccess(ctx, db, userID, workspaceID, channelID, access.HumanAction{Resource: access.ResourceChannel, Action: access.ActionRead})
			if err != nil {
				return channel.HumanChannelAccess{}, mapAccessError(err)
			}
			return channel.HumanChannelAccess{
				Channel: channel.Channel{Type: strings.TrimSpace(authorized.Channel.Type)},
			}, nil
		},
		EnsureChannelScopeParticipantProjection: func(ctx context.Context, execer coredb.QueryExecer, channelID string, memberID string, deliveryPolicy string, now time.Time) error {
			if graph.PrivateAgents == nil {
				return fmt.Errorf("private-agent projection store is not configured")
			}
			return graph.PrivateAgents.EnsureChannelScopeParticipantProjection(ctx, execer, channelID, memberID, deliveryPolicy, now)
		},
		MarkScopeParticipantProjectionRemoved: func(ctx context.Context, execer coredb.QueryExecer, channelID string, memberID string, now time.Time) error {
			if graph.PrivateAgents == nil {
				return fmt.Errorf("private-agent projection store is not configured")
			}
			return graph.PrivateAgents.MarkScopeParticipantProjectionRemoved(ctx, execer, channelID, memberID, now)
		},
		RemovePrivateEntriesForHostChannel: func(ctx context.Context, execer coredb.QueryExecer, ownerMemberID string, hostChannelID string, now time.Time) error {
			if graph.PrivateAgents == nil {
				return fmt.Errorf("private-agent projection store is not configured")
			}
			return graph.PrivateAgents.RemovePrivateEntriesForOwnerHostChannel(ctx, execer, ownerMemberID, hostChannelID, now)
		},
		CheckWorkspaceQuotaInTx: checkWorkspaceQuotaInTx,
	})
	graph.Reaction = graph.App.Reaction()
	graph.Rewind = rewind.New(db, rewind.Options{
		LookupMessageChannelID: graph.App.LookupMessageChannelID,
		ResolveAuthorizedHumanMessageTarget: func(ctx context.Context, userID string, workspaceID string, channelID string) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
			return graph.App.ResolveAuthorizedHumanMessageTarget(ctx, userID, workspaceID, channelID)
		},
		RequireAgentOperation:          graph.App.RequireAgentOperation,
		ReplaceReplyThreadParticipants: message.ReplaceReplyThreadParticipants,
		MergeReplyThreadParticipantIDs: message.MergeReplyThreadParticipantIDs,
	})
	graph.Review = review.New(db, review.Options{
		LoadAuthorizedHumanThreadAccess: func(ctx context.Context, queryer review.Queryer, userID string, workspaceID string, threadChannelID string, action access.HumanAction) (access.HumanThreadAccess, error) {
			return graph.App.LoadAuthorizedHumanThreadAccess(ctx, queryer, userID, workspaceID, threadChannelID, action)
		},
		RequireAgentReviewRead: func(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error) {
			return graph.App.RequireAgentReviewRead(ctx, userID, workspaceID, agentID)
		},
		RequireAgentOperation: func(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error) {
			return graph.App.RequireAgentOperation(ctx, userID, workspaceID, agentID)
		},
		AgentByID: func(ctx context.Context, queryer review.Queryer, workspaceID string, agentID string) (agent.AgentView, error) {
			return graph.App.AgentViewByID(ctx, queryer, workspaceID, agentID)
		},
	})
	graph.Agent = agent.NewWithOptions(db, agent.Options{
		CredentialStorageKey:        append([]byte(nil), options.AppCredentialStorageKey...),
		LegacyCredentialStorageKeys: cloneCredentialStorageKeys(options.LegacyAppCredentialStorageKeys),
	})
	if err := graph.Validate(); err != nil {
		panic(fmt.Sprintf("store graph: %v", err))
	}
	return graph
}

func (g *Graph) Validate() error {
	var errs []string
	check := func(err error) {
		if err != nil {
			errs = append(errs, err.Error())
		}
	}
	check(g.Admin.Validate())
	check(g.Approval.Validate())
	check(g.PrivateAgents.Validate())
	check(g.Channel.Validate())
	check(g.Identity.Validate())
	check(g.IM.Validate())
	check(g.Provider.Validate())
	check(g.Reaction.Validate())
	check(g.Review.Validate())
	check(g.Rewind.Validate())
	check(g.ServiceAccount.Validate())
	check(g.ServiceActor.Validate())
	check(g.Workspace.Validate())
	if len(errs) > 0 {
		return fmt.Errorf("dependency validation failed:\n  %s", strings.Join(errs, "\n  "))
	}
	return nil
}

func mapAccessError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, access.ErrForbidden) {
		return storeerr.ErrForbidden
	}
	if errors.Is(err, access.ErrNotFound) {
		return storeerr.ErrNotFound
	}
	return err
}
