package exec

import (
	"context"
	"encoding/json"
	"strconv"
	"strings"

	invopopjsonschema "github.com/invopop/jsonschema"
	orderedmap "github.com/wk8/go-ordered-map/v2"
)

// Channel management Exec contract (V1, #t2344). These typed DTOs and
// actionSpecs define the `agentteam exec channel` domain published atomically
// through DefaultCatalog and the seven literal routes.
//
// V1 covers structural management only: reads (list/get/members) and direct
// synchronous mutations (create/update/agent-add/agent-remove). delete and
// archive/unarchive are intentionally out of scope. Mutations are direct,
// in-transaction, and carry a requestId for durable idempotency (workspace +
// applicantAgent + requestId; cross-action reuse is a 409 conflict). update
// uses optimistic concurrency via expectedVersion. Identity (workspace, agent
// member, resolved owner) is server-derived and never model-authored.

// --- shared projection DTOs ---

type channelSummary struct {
	ChannelID   string `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Slug        string `json:"slug" jsonschema:"minLength=1,pattern=.*\\S.*"`
	DisplayName string `json:"displayName"`
	Description string `json:"description"`
	Visibility  string `json:"visibility" jsonschema:"enum=private,enum=public"`
	Version     int64  `json:"version" jsonschema:"minimum=1"`
}

type channelMember struct {
	MemberID    string `json:"memberId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	DisplayName string `json:"displayName"`
	Kind        string `json:"kind" jsonschema:"enum=human,enum=agent,enum=service"`
}

// --- request DTOs ---

type channelListInput struct {
	Limit  int    `json:"limit,omitempty" jsonschema:"minimum=1,maximum=100"`
	Cursor string `json:"cursor,omitempty" jsonschema:"maxLength=32"`
}

type channelGetInput struct {
	ChannelID string `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
}

type channelMembersInput struct {
	ChannelID string `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Limit     int    `json:"limit,omitempty" jsonschema:"minimum=1,maximum=100"`
	Cursor    string `json:"cursor,omitempty" jsonschema:"maxLength=32"`
}

type channelCreateInput struct {
	RequestID   string  `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	DisplayName string  `json:"displayName" jsonschema:"minLength=1,maxLength=255,pattern=.*\\S.*"`
	Description *string `json:"description,omitempty"`
	Visibility  string  `json:"visibility,omitempty" jsonschema:"enum=private,enum=public"`
}

type channelUpdateInput struct {
	RequestID       string  `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	ChannelID       string  `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	ExpectedVersion int64   `json:"expectedVersion" jsonschema:"minimum=1"`
	DisplayName     *string `json:"displayName,omitempty"`
	Description     *string `json:"description,omitempty"`
	Visibility      string  `json:"visibility,omitempty" jsonschema:"enum=private,enum=public"`
}

// JSONSchema expresses update's "at least one mutation" rule via the invopop
// jsonschema override (same mechanism as roleUpdateRequest): a closed object,
// the three fence fields required, and minProperties=4 so an update carrying
// only the required fields (no mutation) is rejected.
func (channelUpdateInput) JSONSchema() *invopopjsonschema.Schema {
	properties := orderedmap.New[string, *invopopjsonschema.Schema]()
	properties.Set("requestId", channelNonBlankStringSchema(128))
	properties.Set("channelId", channelNonBlankStringSchema(0))
	properties.Set("expectedVersion", channelIntegerSchema(1))
	properties.Set("displayName", channelNonBlankStringSchema(255))
	properties.Set("description", &invopopjsonschema.Schema{Type: "string"})
	properties.Set("visibility", &invopopjsonschema.Schema{Type: "string", Enum: []any{"private", "public"}})
	minProperties := uint64(4)
	return &invopopjsonschema.Schema{
		Type:                 "object",
		Properties:           properties,
		Required:             []string{"requestId", "channelId", "expectedVersion"},
		MinProperties:        &minProperties,
		AdditionalProperties: invopopjsonschema.FalseSchema,
	}
}

// channelAgentReference identifies one agent member target for an agent-add or
// agent-remove mutation. Human members are intentionally not addable by an
// Agent; humans join channels via the share-card self-service flow.
type channelAgentReference struct {
	AgentID string `json:"agentId" jsonschema:"minLength=1,maxLength=64,pattern=.*\\S.*"`
}

type channelAgentAddInput struct {
	RequestID string                  `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	ChannelID string                  `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Agents    []channelAgentReference `json:"agents" jsonschema:"minItems=1,maxItems=100"`
}

type channelAgentRemoveInput struct {
	RequestID string                  `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	ChannelID string                  `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Agents    []channelAgentReference `json:"agents" jsonschema:"minItems=1,maxItems=100"`
}

func channelNonBlankStringSchema(maxLength uint64) *invopopjsonschema.Schema {
	minLength := uint64(1)
	schema := &invopopjsonschema.Schema{Type: "string", MinLength: &minLength, Pattern: `.*\S.*`}
	if maxLength > 0 {
		schema.MaxLength = &maxLength
	}
	return schema
}

func channelIntegerSchema(minimum int64) *invopopjsonschema.Schema {
	return &invopopjsonschema.Schema{Type: "integer", Minimum: json.Number(strconv.FormatInt(minimum, 10))}
}

// --- wire request DTOs (sent to the Agent runtime) ---

type channelCreateRequest struct {
	RequestID   string  `json:"requestId"`
	DisplayName string  `json:"displayName"`
	Description *string `json:"description,omitempty"`
	Visibility  string  `json:"visibility,omitempty"`
}

type channelUpdateRequest struct {
	RequestID       string  `json:"requestId"`
	ChannelID       string  `json:"channelId"`
	ExpectedVersion int64   `json:"expectedVersion"`
	DisplayName     *string `json:"displayName,omitempty"`
	Description     *string `json:"description,omitempty"`
	Visibility      string  `json:"visibility,omitempty"`
}

type channelAgentMutationRequest struct {
	RequestID string                  `json:"requestId"`
	ChannelID string                  `json:"channelId"`
	Agents    []channelAgentReference `json:"agents"`
}

// --- result DTOs ---

type channelListItem struct {
	ChannelID   string `json:"channelId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Slug        string `json:"slug" jsonschema:"minLength=1,pattern=.*\\S.*"`
	DisplayName string `json:"displayName"`
	Description string `json:"description"`
	Visibility  string `json:"visibility" jsonschema:"enum=private,enum=public"`
}

type channelListOutput struct {
	Channels   []channelListItem `json:"channels"`
	HasMore    bool              `json:"hasMore"`
	NextCursor string            `json:"nextCursor,omitempty"`
	TotalCount int               `json:"totalCount"`
}

type channelGetOutput struct {
	Channel channelSummary `json:"channel"`
}

type channelMembersOutput struct {
	ChannelID  string          `json:"channelId"`
	Members    []channelMember `json:"members"`
	HasMore    bool            `json:"hasMore"`
	NextCursor string          `json:"nextCursor,omitempty"`
	TotalCount int             `json:"totalCount"`
}

type channelCreateOutput struct {
	Channel channelSummary `json:"channel"`
}

type channelUpdateOutput struct {
	Channel channelSummary `json:"channel"`
}

// channelAgentMember is one affected agent in an agent-add/agent-remove result.
// kind is omitted (always "agent") per the agent-only contract.
type channelAgentMember struct {
	MemberID    string `json:"memberId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	DisplayName string `json:"displayName"`
}

type channelAgentMutationOutput struct {
	ChannelID string               `json:"channelId"`
	Agents    []channelAgentMember `json:"agents"`
}

type channelRuntime interface {
	list(context.Context, channelListInput) (channelListOutput, error)
	get(context.Context, string) (channelGetOutput, error)
	members(context.Context, channelMembersInput) (channelMembersOutput, error)
	create(context.Context, channelCreateRequest) (channelCreateOutput, error)
	update(context.Context, string, channelUpdateRequest) (channelUpdateOutput, error)
	agentAdd(context.Context, string, channelAgentMutationRequest) (channelAgentMutationOutput, error)
	agentRemove(context.Context, string, channelAgentMutationRequest) (channelAgentMutationOutput, error)
}

type channelHandler struct {
	remote  channelRuntime
	read    remoteReadTransport
	receipt requestReceiptTransport
}

func (handler channelHandler) list(ctx context.Context, request actionScopedJSON[channelListInput]) (actionResult[channelListOutput], error) {
	input := request.Body
	input.Cursor = strings.TrimSpace(input.Cursor)
	result, err := handler.runtime(request.Authorized).list(ctx, input)
	if err != nil {
		return actionResult[channelListOutput]{}, classifyAPIError(err, "list Channels failed")
	}
	if result.Channels == nil {
		result.Channels = []channelListItem{}
	}
	return actionResult[channelListOutput]{Data: result, Text: "Listed Channels."}, nil
}

func (handler channelHandler) get(ctx context.Context, request actionScopedJSON[channelGetInput]) (actionResult[channelGetOutput], error) {
	channelID := strings.TrimSpace(request.Body.ChannelID)
	if channelID == "" {
		return actionResult[channelGetOutput]{}, NewError(CodeValidation, "channelId is required")
	}
	result, err := handler.runtime(request.Authorized).get(ctx, channelID)
	if err != nil {
		return actionResult[channelGetOutput]{}, classifyAPIError(err, "get Channel failed")
	}
	return actionResult[channelGetOutput]{Data: result, Text: "Fetched Channel."}, nil
}

func (handler channelHandler) members(ctx context.Context, request actionScopedJSON[channelMembersInput]) (actionResult[channelMembersOutput], error) {
	channelID := strings.TrimSpace(request.Body.ChannelID)
	if channelID == "" {
		return actionResult[channelMembersOutput]{}, NewError(CodeValidation, "channelId is required")
	}
	result, err := handler.runtime(request.Authorized).members(ctx, channelMembersInput{
		ChannelID: channelID,
		Limit:     request.Body.Limit,
		Cursor:    strings.TrimSpace(request.Body.Cursor),
	})
	if err != nil {
		return actionResult[channelMembersOutput]{}, classifyAPIError(err, "list Channel members failed")
	}
	if result.Members == nil {
		result.Members = []channelMember{}
	}
	return actionResult[channelMembersOutput]{Data: result, Text: "Listed Channel members."}, nil
}

func (handler channelHandler) create(ctx context.Context, request actionScopedJSON[channelCreateInput]) (actionResult[channelCreateOutput], error) {
	input := request.Body
	requestID := strings.TrimSpace(input.RequestID)
	if requestID == "" {
		return actionResult[channelCreateOutput]{}, NewError(CodeValidation, "requestId is required")
	}
	result, err := handler.runtime(request.Authorized).create(ctx, channelCreateRequest{
		RequestID: requestID, DisplayName: strings.TrimSpace(input.DisplayName),
		Description: trimChannelOptionalString(input.Description), Visibility: input.Visibility,
	})
	if err != nil {
		return actionResult[channelCreateOutput]{}, classifyAPIError(err, "create Channel failed")
	}
	return actionResult[channelCreateOutput]{Data: result, Text: "Created Channel."}, nil
}

func (handler channelHandler) update(ctx context.Context, request actionScopedJSON[channelUpdateInput]) (actionResult[channelUpdateOutput], error) {
	input := request.Body
	channelID := strings.TrimSpace(input.ChannelID)
	requestID := strings.TrimSpace(input.RequestID)
	if channelID == "" || requestID == "" {
		return actionResult[channelUpdateOutput]{}, NewError(CodeValidation, "channelId and requestId are required")
	}
	result, err := handler.runtime(request.Authorized).update(ctx, channelID, channelUpdateRequest{
		ChannelID: channelID, RequestID: requestID, ExpectedVersion: input.ExpectedVersion,
		DisplayName: trimChannelOptionalString(input.DisplayName), Description: trimChannelOptionalString(input.Description),
		Visibility: input.Visibility,
	})
	if err != nil {
		return actionResult[channelUpdateOutput]{}, classifyAPIError(err, "update Channel failed")
	}
	return actionResult[channelUpdateOutput]{Data: result, Text: "Updated Channel."}, nil
}

func (handler channelHandler) agentAdd(ctx context.Context, request actionScopedJSON[channelAgentAddInput]) (actionResult[channelAgentMutationOutput], error) {
	input := request.Body
	channelID := strings.TrimSpace(input.ChannelID)
	requestID := strings.TrimSpace(input.RequestID)
	if channelID == "" || requestID == "" {
		return actionResult[channelAgentMutationOutput]{}, NewError(CodeValidation, "channelId and requestId are required")
	}
	result, err := handler.runtime(request.Authorized).agentAdd(ctx, channelID, channelAgentMutationRequest{
		ChannelID: channelID, RequestID: requestID, Agents: input.Agents,
	})
	if err != nil {
		return actionResult[channelAgentMutationOutput]{}, classifyAPIError(err, "add Channel agents failed")
	}
	if result.Agents == nil {
		result.Agents = []channelAgentMember{}
	}
	return actionResult[channelAgentMutationOutput]{Data: result, Text: "Added Channel agents."}, nil
}

func (handler channelHandler) agentRemove(ctx context.Context, request actionScopedJSON[channelAgentRemoveInput]) (actionResult[channelAgentMutationOutput], error) {
	input := request.Body
	channelID := strings.TrimSpace(input.ChannelID)
	requestID := strings.TrimSpace(input.RequestID)
	if channelID == "" || requestID == "" {
		return actionResult[channelAgentMutationOutput]{}, NewError(CodeValidation, "channelId and requestId are required")
	}
	result, err := handler.runtime(request.Authorized).agentRemove(ctx, channelID, channelAgentMutationRequest{
		ChannelID: channelID, RequestID: requestID, Agents: input.Agents,
	})
	if err != nil {
		return actionResult[channelAgentMutationOutput]{}, classifyAPIError(err, "remove Channel agents failed")
	}
	if result.Agents == nil {
		result.Agents = []channelAgentMember{}
	}
	return actionResult[channelAgentMutationOutput]{Data: result, Text: "Removed Channel agents."}, nil
}

func (handler channelHandler) runtime(authorized runtimeAuthorization) channelRuntime {
	if handler.remote != nil {
		return handler.remote
	}
	return channelRemote{client: authorized.Server, read: handler.read, receipt: handler.receipt}
}

func trimChannelOptionalString(value *string) *string {
	if value == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*value)
	return &trimmed
}

// --- action specs ---

var channelListSpec = actionSpec[channelListInput, channelListOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "list",
		Summary:    "list Channels the current Agent is a member of",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[channelListInput]{
		{Title: "joined Channels", Input: channelListInput{Limit: 50}},
	},
}

var channelGetSpec = actionSpec[channelGetInput, channelGetOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "get",
		Summary:    "get one Channel's profile and optimistic-concurrency version",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[channelGetInput]{
		{Title: "by id", Input: channelGetInput{ChannelID: "chn_0123456789abcd"}},
	},
}

var channelMembersSpec = actionSpec[channelMembersInput, channelMembersOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "members",
		Summary:    "list the members of one Channel",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[channelMembersInput]{
		{Title: "members", Input: channelMembersInput{ChannelID: "chn_0123456789abcd", Limit: 50}},
	},
}

var channelCreateSpec = actionSpec[channelCreateInput, channelCreateOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "create",
		Summary:    "create a new Channel owned by the current Agent's owner",
		Permission: permissionAgentScopedMutation,
	},
	Examples: []actionExample[channelCreateInput]{
		{Title: "create channel", Input: channelCreateInput{
			RequestID: "req-channel-create-01", DisplayName: "release-room",
		}},
	},
}

var channelUpdateSpec = actionSpec[channelUpdateInput, channelUpdateOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "update",
		Summary:    "update one Channel's profile or visibility with optimistic concurrency",
		Permission: permissionAgentScopedMutation,
	},
	Examples: []actionExample[channelUpdateInput]{
		{Title: "rename channel", Input: channelUpdateInput{
			RequestID: "req-channel-update-01", ChannelID: "chn_0123456789abcd",
			ExpectedVersion: 1, DisplayName: ptrChannelString("renamed-room"),
		}},
	},
}

var channelAgentAddSpec = actionSpec[channelAgentAddInput, channelAgentMutationOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "agent-add",
		Summary:    "add agent members to one Channel",
		Permission: permissionAgentScopedMutation,
	},
	Examples: []actionExample[channelAgentAddInput]{
		{Title: "add an agent", Input: channelAgentAddInput{
			RequestID: "req-channel-addagent-01", ChannelID: "chn_0123456789abcd",
			Agents: []channelAgentReference{{AgentID: "agt_0123456789abcd"}},
		}},
	},
}

var channelAgentRemoveSpec = actionSpec[channelAgentRemoveInput, channelAgentMutationOutput]{
	Meta: actionMeta{
		Domain:     "channel",
		Name:       "agent-remove",
		Summary:    "remove agent members from one Channel",
		Permission: permissionAgentScopedMutation,
	},
	Examples: []actionExample[channelAgentRemoveInput]{
		{Title: "remove an agent", Input: channelAgentRemoveInput{
			RequestID: "req-channel-rmagent-01", ChannelID: "chn_0123456789abcd",
			Agents: []channelAgentReference{{AgentID: "agt_0123456789abcd"}},
		}},
	},
}

func ptrChannelString(value string) *string { return &value }
