package exec

import (
	"context"
	"strings"

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

// Role management Exec contract (#t2249 V1). These typed DTOs and actionSpecs
// define the `agentteam exec role` domain published atomically through DefaultCatalog
// and the seven literal routes. Mutation authority remains in the Human
// Approval + durable Role runtime; Desktop only exposes the typed transport.
//
// Idempotency domain: workspace + applicantAgent + requestId (action enters the
// normalized payload digest, so cross-action reuse is a 409 conflict). The
// internal approval state `approved` is never a public status; the public
// request state machine is pending | processing | succeeded | rejected |
// failed | cancelled. delete is soft-delete (hide) only.

// --- shared projection DTOs ---

type roleSummary struct {
	RoleID        string `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Name          string `json:"name" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Description   string `json:"description,omitempty"`
	Visibility    string `json:"visibility" jsonschema:"enum=private,enum=workspace"`
	RoleVersionID string `json:"roleVersionId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	CanManage     bool   `json:"canManage"`
}

type roleDetail struct {
	RoleID        string `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Name          string `json:"name" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Description   string `json:"description,omitempty"`
	Definition    string `json:"definition"`
	Visibility    string `json:"visibility" jsonschema:"enum=private,enum=workspace"`
	RoleVersionID string `json:"roleVersionId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	CanManage     bool   `json:"canManage"`
}

// roleFailure is the stable, secret-free failure shape exposed by request-get.
// It never carries payload, receipt, or internal error text.
type roleFailure struct {
	Code    string `json:"code" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Message string `json:"message"`
}

// --- request DTOs ---

type roleListRequest struct {
	View   string `json:"view,omitempty" jsonschema:"enum=all,enum=workspace,enum=created_by_me,enum=private"`
	Query  string `json:"query,omitempty"`
	Limit  int    `json:"limit,omitempty" jsonschema:"minimum=1,maximum=100"`
	Cursor string `json:"cursor,omitempty"`
}

type roleGetRequest struct {
	RoleID string `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
}

type roleRequestGetRequest struct {
	RequestID string `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
}

type roleCreateRequest struct {
	RequestID   string `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	Name        string `json:"name" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Definition  string `json:"definition" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Description string `json:"description,omitempty"`
}

type roleUpdateRequest struct {
	RequestID             string  `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	RoleID                string  `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	ExpectedRoleVersionID string  `json:"expectedRoleVersionId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Name                  *string `json:"name,omitempty"`
	Description           *string `json:"description,omitempty"`
	Definition            *string `json:"definition,omitempty"`
	Visibility            string  `json:"visibility,omitempty" jsonschema:"enum=private,enum=workspace"`
}

// JSONSchema expresses update's "at least one mutation" rule via the existing
// invopop jsonschema override (the same mechanism agentUpdateRequest uses): a
// closed object, the three fence fields required, and minProperties=4 so an
// update carrying only the required fields (no mutation) is rejected. No
// Role-specific schema bypass is introduced.
func (roleUpdateRequest) JSONSchema() *invopopjsonschema.Schema {
	properties := orderedmap.New[string, *invopopjsonschema.Schema]()
	properties.Set("requestId", roleNonBlankStringSchema(128))
	properties.Set("roleId", roleNonBlankStringSchema(0))
	properties.Set("expectedRoleVersionId", roleNonBlankStringSchema(0))
	// name/definition are non-blank when present (canonical Role service rejects
	// blank name/definition); description allows "" so an explicit empty clears it.
	properties.Set("name", roleNonBlankStringSchema(0))
	properties.Set("description", &invopopjsonschema.Schema{Type: "string"})
	properties.Set("definition", roleNonBlankStringSchema(0))
	properties.Set("visibility", &invopopjsonschema.Schema{Type: "string", Enum: []any{"private", "workspace"}})
	minProperties := uint64(4)
	return &invopopjsonschema.Schema{
		Type:                 "object",
		Properties:           properties,
		Required:             []string{"requestId", "roleId", "expectedRoleVersionId"},
		MinProperties:        &minProperties,
		AdditionalProperties: invopopjsonschema.FalseSchema,
	}
}

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

// roleString returns a pointer to value, used to populate *string update fields
// (nil means "keep current"; a non-nil pointer sets — or clears when empty).
func roleString(value string) *string { return &value }

type roleDeleteRequest struct {
	RequestID             string `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	RoleID                string `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	ExpectedRoleVersionID string `json:"expectedRoleVersionId" jsonschema:"minLength=1,pattern=.*\\S.*"`
}

type roleApplyRequest struct {
	RequestID     string `json:"requestId" jsonschema:"minLength=1,maxLength=128,pattern=.*\\S.*"`
	RoleID        string `json:"roleId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	RoleVersionID string `json:"roleVersionId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	AgentID       string `json:"agentId" jsonschema:"minLength=1,pattern=.*\\S.*"`
}

// --- result DTOs ---

type roleListResult struct {
	Roles      []roleSummary `json:"roles"`
	NextCursor string        `json:"nextCursor,omitempty"`
}

type roleGetResult struct {
	Role roleDetail `json:"role"`
}

// roleRequestResponse is the {approvalId, status} handle returned by the four
// Human-approval mutations (create/update/delete/apply).
type roleRequestResponse struct {
	ApprovalID string `json:"approvalId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Status     string `json:"status" jsonschema:"enum=pending,enum=processing,enum=succeeded,enum=rejected,enum=failed,enum=cancelled"`
}

type roleRequestGetResult struct {
	RequestID     string       `json:"requestId" jsonschema:"minLength=1,pattern=.*\\S.*"`
	Action        string       `json:"action" jsonschema:"enum=create,enum=update,enum=delete,enum=apply"`
	Status        string       `json:"status" jsonschema:"enum=pending,enum=processing,enum=succeeded,enum=rejected,enum=failed,enum=cancelled"`
	ApprovalID    string       `json:"approvalId,omitempty"`
	RoleID        string       `json:"roleId,omitempty"`
	RoleVersionID string       `json:"roleVersionId,omitempty"`
	AgentID       string       `json:"agentId,omitempty"`
	Failure       *roleFailure `json:"failure,omitempty"`
}

type roleRuntime interface {
	list(context.Context, roleListRequest) (roleListResult, error)
	get(context.Context, roleGetRequest) (roleGetResult, error)
	requestGet(context.Context, roleRequestGetRequest) (roleRequestGetResult, error)
	create(context.Context, roleCreateRequest) (roleRequestResponse, error)
	update(context.Context, roleUpdateRequest) (roleRequestResponse, error)
	delete(context.Context, roleDeleteRequest) (roleRequestResponse, error)
	apply(context.Context, roleApplyRequest) (roleRequestResponse, error)
}

type roleHandler struct {
	remote  roleRuntime
	read    remoteReadTransport
	receipt requestReceiptTransport
}

func (handler roleHandler) list(ctx context.Context, request actionScopedJSON[roleListRequest]) (actionResult[roleListResult], error) {
	result, err := handler.runtime(request.Authorized).list(ctx, request.Body)
	if err != nil {
		return actionResult[roleListResult]{}, classifyAPIError(err, "list Roles failed")
	}
	if result.Roles == nil {
		result.Roles = []roleSummary{}
	}
	return actionResult[roleListResult]{Data: result, Text: "Listed Roles."}, nil
}

func (handler roleHandler) get(ctx context.Context, request actionScopedJSON[roleGetRequest]) (actionResult[roleGetResult], error) {
	if strings.TrimSpace(request.Body.RoleID) == "" {
		return actionResult[roleGetResult]{}, NewError(CodeValidation, "roleId is required")
	}
	result, err := handler.runtime(request.Authorized).get(ctx, request.Body)
	if err != nil {
		return actionResult[roleGetResult]{}, classifyAPIError(err, "get Role failed")
	}
	return actionResult[roleGetResult]{Data: result, Text: "Fetched Role."}, nil
}

func (handler roleHandler) requestGet(ctx context.Context, request actionScopedJSON[roleRequestGetRequest]) (actionResult[roleRequestGetResult], error) {
	if strings.TrimSpace(request.Body.RequestID) == "" {
		return actionResult[roleRequestGetResult]{}, NewError(CodeValidation, "requestId is required")
	}
	result, err := handler.runtime(request.Authorized).requestGet(ctx, request.Body)
	if err != nil {
		return actionResult[roleRequestGetResult]{}, classifyAPIError(err, "get Role request failed")
	}
	return actionResult[roleRequestGetResult]{Data: result, Text: "Fetched Role request."}, nil
}

func (handler roleHandler) create(ctx context.Context, request actionScopedJSON[roleCreateRequest]) (actionResult[roleRequestResponse], error) {
	result, err := handler.runtime(request.Authorized).create(ctx, request.Body)
	return roleMutationActionResult(result, err, "request Role creation failed", "Requested Role creation.")
}

func (handler roleHandler) update(ctx context.Context, request actionScopedJSON[roleUpdateRequest]) (actionResult[roleRequestResponse], error) {
	result, err := handler.runtime(request.Authorized).update(ctx, request.Body)
	return roleMutationActionResult(result, err, "request Role update failed", "Requested Role update.")
}

func (handler roleHandler) delete(ctx context.Context, request actionScopedJSON[roleDeleteRequest]) (actionResult[roleRequestResponse], error) {
	result, err := handler.runtime(request.Authorized).delete(ctx, request.Body)
	return roleMutationActionResult(result, err, "request Role deletion failed", "Requested Role deletion.")
}

func (handler roleHandler) apply(ctx context.Context, request actionScopedJSON[roleApplyRequest]) (actionResult[roleRequestResponse], error) {
	result, err := handler.runtime(request.Authorized).apply(ctx, request.Body)
	return roleMutationActionResult(result, err, "request Role apply failed", "Requested Role apply.")
}

func (handler roleHandler) runtime(authorized runtimeAuthorization) roleRuntime {
	if handler.remote != nil {
		return handler.remote
	}
	return roleRemote{client: authorized.Server, read: handler.read, receipt: handler.receipt}
}

func roleMutationActionResult(
	result roleRequestResponse,
	err error,
	problem, text string,
) (actionResult[roleRequestResponse], error) {
	if err != nil {
		return actionResult[roleRequestResponse]{}, classifyAPIError(err, problem)
	}
	return actionResult[roleRequestResponse]{Data: result, Text: text}, nil
}

// --- action specs ---

var roleListSpec = actionSpec[roleListRequest, roleListResult]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "list",
		Summary:    "list Roles available to the current Human",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[roleListRequest]{
		{Title: "all Roles", Input: roleListRequest{View: "all"}},
	},
}

var roleGetSpec = actionSpec[roleGetRequest, roleGetResult]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "get",
		Summary:    "get a Role's active version detail",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[roleGetRequest]{
		{Title: "by id", Input: roleGetRequest{RoleID: "role_123"}},
	},
}

var roleRequestGetSpec = actionSpec[roleRequestGetRequest, roleRequestGetResult]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "request-get",
		Summary:    "read the public state of a Role request the same Agent submitted",
		Permission: permissionReadOnly,
	},
	Examples: []actionExample[roleRequestGetRequest]{
		{Title: "by request id", Input: roleRequestGetRequest{RequestID: "req-role-01"}},
	},
}

var roleCreateSpec = actionSpec[roleCreateRequest, roleRequestResponse]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "create",
		Summary:    "request approval to create a private Role owned by the same Human",
		Permission: permissionExistingGate,
	},
	Examples: []actionExample[roleCreateRequest]{
		{Title: "create role", Input: roleCreateRequest{
			RequestID: "req-role-create-01", Name: "release-helper", Definition: "skills: []",
		}},
	},
}

var roleUpdateSpec = actionSpec[roleUpdateRequest, roleRequestResponse]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "update",
		Summary:    "request approval to update a Role owned or managed by the same Human",
		Permission: permissionExistingGate,
	},
	Examples: []actionExample[roleUpdateRequest]{
		{Title: "rename role", Input: roleUpdateRequest{
			RequestID: "req-role-update-01", RoleID: "role_123",
			ExpectedRoleVersionID: "ver_1", Name: roleString("renamed-helper"),
		}},
	},
}

var roleDeleteSpec = actionSpec[roleDeleteRequest, roleRequestResponse]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "delete",
		Summary:    "request approval to soft-delete (hide) a Role owned or managed by the same Human",
		Permission: permissionExistingGate,
	},
	Examples: []actionExample[roleDeleteRequest]{
		{Title: "delete role", Input: roleDeleteRequest{
			RequestID: "req-role-delete-01", RoleID: "role_123", ExpectedRoleVersionID: "ver_1",
		}},
	},
}

var roleApplySpec = actionSpec[roleApplyRequest, roleRequestResponse]{
	Meta: actionMeta{
		Domain:     "role",
		Name:       "apply",
		Summary:    "request approval to apply a fixed Role version to an Agent owned by the same Human",
		Permission: permissionExistingGate,
	},
	Examples: []actionExample[roleApplyRequest]{
		{Title: "apply role to agent", Input: roleApplyRequest{
			RequestID: "req-role-apply-01", RoleID: "role_123",
			RoleVersionID: "ver_1", AgentID: "agt_123",
		}},
	},
}
