package ones

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

// Handlers returns the Action dispatch table. The keys must exactly match
// the Definition catalog; bootstrap verifies the coverage.
func Handlers() map[string]runner.RESTHandler {
	return map[string]runner.RESTHandler{
		ActionGetAuthInfo:      handleGetAuthInfo,
		ActionListProjects:     handleListProjects,
		ActionCreateProject:    handleCreateProject,
		ActionListIssueTypes:   handleListIssueTypes,
		ActionListTaskStatuses: handleListTaskStatuses,
		ActionCreateTask:       handleCreateTask,
		ActionUpdateTask:       handleUpdateTask,
		ActionGetTask:          handleGetTask,
		ActionGetTasks:         handleGetTasks,
		ActionDeleteTask:       handleDeleteTask,
		ActionFilterTasks:      handleFilterTasks,
		ActionListSprints:      handleListSprints,
		ActionCreateSprint:     handleCreateSprint,
		ActionBindTaskPage:     handleBindTaskPage,
		ActionUnbindTaskPage:   handleUnbindTaskPage,
		ActionListSpaces:       handleListSpaces,
		ActionCreateSpace:      handleCreateSpace,
		ActionGetSpace:         handleGetSpace,
		ActionUpdateSpace:      handleUpdateSpace,
		ActionDeleteSpace:      handleDeleteSpace,
		ActionListPages:        handleListPages,
		ActionGetPage:          handleGetPage,
		ActionCreatePage:       handleCreatePage,
		ActionUpdatePage:       handleUpdatePage,
		ActionDeletePage:       handleDeletePage,
	}
}

// Route-parameter shapes, calibrated to the official uuid widths: 8
// characters for team/user/wiki identifiers, 16 for project and work item
// uuids, and the work-item route also accepts a decimal serial number.
var (
	pathParam8Pattern  = regexp.MustCompile(`^[A-Za-z0-9]{8}$`)
	pathParam16Pattern = regexp.MustCompile(`^[A-Za-z0-9]{16}$`)
	taskRoutePattern   = regexp.MustCompile(`^(?:[A-Za-z0-9]{16}|[0-9]{1,10})$`)
)

// request builds one ONES request: the Referer header mirrors the connected
// host origin, matching the ONES auth contract.
func request(
	credentials runner.Credentials,
	method string,
	path string,
	query url.Values,
	payload any,
) runner.RESTRequest {
	return runner.RESTRequest{
		Method:  method,
		Path:    path,
		Query:   query,
		Body:    payload,
		Headers: map[string]string{"Referer": credentials[BaseURLField]},
	}
}

// pathParam8 validates one 8-character route parameter and fails closed on
// anything the pattern rejects.
func pathParam8(name, value string) (string, error) {
	if !pathParam8Pattern.MatchString(value) {
		return "", runner.Errorf(
			runner.ErrorValidation, fmt.Sprintf("%s is not a valid ONES uuid", name), nil)
	}
	return value, nil
}

// pathParam16 validates one 16-character route parameter and fails closed on
// anything the pattern rejects.
func pathParam16(name, value string) (string, error) {
	if !pathParam16Pattern.MatchString(value) {
		return "", runner.Errorf(
			runner.ErrorValidation, fmt.Sprintf("%s is not a valid ONES uuid", name), nil)
	}
	return value, nil
}

// taskRouteParam validates one work-item route identifier: the 16-character
// uuid or a decimal serial number.
func taskRouteParam(name, value string) (string, error) {
	if !taskRoutePattern.MatchString(value) {
		return "", runner.Errorf(
			runner.ErrorValidation, fmt.Sprintf("%s is not a valid ONES task identifier", name), nil)
	}
	return value, nil
}

func decodeInput(input json.RawMessage, target any) error {
	if len(input) == 0 {
		return nil
	}
	if err := json.Unmarshal(input, target); err != nil {
		return runner.Errorf(runner.ErrorValidation, "action input was not decodable", err)
	}
	return nil
}

type teamScoped struct {
	TeamUUID string `json:"team_uuid"`
}

// taskIdentifier is the task-detail route identifier: the documented union
// of a uuid and a decimal serial number, normalized to its route string.
type taskIdentifier struct {
	raw string
}

// UnmarshalJSON accepts the uuid string or integer serial number lexical the
// schema admits; anything else is rejected at decode time.
func (identifier *taskIdentifier) UnmarshalJSON(data []byte) error {
	trimmed := strings.TrimSpace(string(data))
	if trimmed == "null" {
		return nil
	}
	if value, err := strconv.Atoi(trimmed); err == nil {
		if value < 0 {
			return fmt.Errorf("task serial number must not be negative")
		}
		identifier.raw = strconv.Itoa(value)
		return nil
	}
	var text string
	if err := json.Unmarshal(data, &text); err != nil {
		return fmt.Errorf("task must be a uuid or serial number")
	}
	identifier.raw = text
	return nil
}

// normalize returns the route string of the identifier.
func (identifier taskIdentifier) normalize() string {
	return identifier.raw
}

func (input teamScoped) team() (string, error) {
	teamUUID := input.TeamUUID
	return pathParam8("team_uuid", teamUUID)
}

// authInfoUser is the known, non-secret projection of the token_info user:
// the login token is not decodable here at all, so it can never leak into
// Agent output or invocation records.
type authInfoUser struct {
	UUID       string `json:"uuid"`
	Email      string `json:"email"`
	Name       string `json:"name"`
	NamePinyin string `json:"name_pinyin"`
	Avatar     string `json:"avatar"`
	Phone      string `json:"phone"`
	Status     int    `json:"status"`
	CreateTime int64  `json:"create_time"`
	Channel    string `json:"channel"`
	License    []int  `json:"license_types,omitempty"`
}

// handleGetAuthInfo answers with a strictly rebuilt token_info envelope:
// only the known user/teams/org projection is returned, the user must carry
// a non-empty uuid, and any malformed envelope fails closed.
func handleGetAuthInfo(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	_ json.RawMessage,
) (json.RawMessage, error) {
	raw, err := client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/auth/token_info", nil, nil))
	if err != nil {
		return nil, err
	}
	var envelope struct {
		User  *authInfoUser     `json:"user"`
		Teams []json.RawMessage `json:"teams"`
		Org   json.RawMessage   `json:"org"`
	}
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "ones auth info response was not usable", err)
	}
	if envelope.User == nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "ones auth info response has no user identity", nil)
	}
	if _, err := pathParam8("user.uuid", envelope.User.UUID); err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "ones auth info response has no valid user identity", nil)
	}
	if envelope.Teams == nil {
		envelope.Teams = []json.RawMessage{}
	}
	org := envelope.Org
	if len(org) == 0 {
		org = json.RawMessage("null")
	}
	return json.Marshal(map[string]any{
		"user":  envelope.User,
		"teams": envelope.Teams,
		"org":   org,
	})
}

func handleListProjects(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		IDs string `json:"ids"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	// The official contract requires ids on /projects/info; listing without
	// a filter must take the dedicated my_project route instead.
	if payload.IDs == "" {
		return client.Call(ctx, credentials, request(credentials, "GET",
			ProjectPrefix+"/team/"+team+"/projects/my_project", nil, nil))
	}
	query := url.Values{}
	query.Set("ids", payload.IDs)
	return client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/team/"+team+"/projects/info", query, nil))
}

func handleCreateProject(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		Project struct {
			UUID   string `json:"uuid"`
			Name   string `json:"name"`
			Assign string `json:"assign"`
		} `json:"project"`
		TemplateID *string  `json:"template_id"`
		Members    []string `json:"members"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	projectUUID, err := pathParam16("uuid", payload.Project.UUID)
	if err != nil {
		return nil, err
	}
	assign, err := pathParam8("assign", payload.Project.Assign)
	if err != nil {
		return nil, err
	}
	project := map[string]any{
		"uuid":   projectUUID,
		"assign": assign,
		"name":   payload.Project.Name,
	}
	body := map[string]any{"project": project}
	if payload.TemplateID != nil {
		body["template_id"] = *payload.TemplateID
	}
	if payload.Members != nil {
		body["members"] = payload.Members
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/projects/add", nil, body))
}

func handleListIssueTypes(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload teamScoped
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	return client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/team/"+team+"/issue_types", nil, nil))
}

func handleListTaskStatuses(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload teamScoped
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	return client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/team/"+team+"/task_statuses", nil, nil))
}

func handleCreateTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	operation := "add2"
	return batchTaskCall(ctx, client, credentials, input, operation)
}

func handleUpdateTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	operation := "update3"
	return batchTaskCall(ctx, client, credentials, input, operation)
}

// batchTaskCall posts the batch create/update contract: the raw tasks array
// rides through untouched and the tasks/bad_tasks envelope comes back whole.
func batchTaskCall(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
	operation string,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		Tasks []json.RawMessage `json:"tasks"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	if len(payload.Tasks) == 0 {
		return nil, runner.Errorf(runner.ErrorValidation, "tasks must not be empty", nil)
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/tasks/"+operation, nil,
		map[string]any{"tasks": payload.Tasks}))
}

func handleGetTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		Task taskIdentifier `json:"task"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	task := payload.Task.normalize()
	if _, err := taskRouteParam("task", task); err != nil {
		return nil, err
	}
	return client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/team/"+team+"/task/"+task+"/info", nil, nil))
}

func handleGetTasks(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		IDs []string `json:"ids"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	if len(payload.IDs) == 0 {
		return nil, runner.Errorf(runner.ErrorValidation, "ids must not be empty", nil)
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/tasks/info", nil,
		map[string]any{"ids": payload.IDs}))
}

func handleDeleteTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		TaskUUID string `json:"task_uuid"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	task, err := pathParam16("task_uuid", payload.TaskUUID)
	if err != nil {
		return nil, err
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/task/"+task+"/delete", nil, nil))
}

func handleFilterTasks(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		Query              json.RawMessage   `json:"query"`
		Sort               []json.RawMessage `json:"sort"`
		GroupBy            *string           `json:"group_by"`
		IncludeSubtasks    *bool             `json:"include_subtasks"`
		WithBoards         *bool             `json:"with_boards"`
		Boards             []json.RawMessage `json:"boards"`
		IncludeStatusUUID  *bool             `json:"include_status_uuid"`
		IncludeIssueType   *bool             `json:"include_issue_type"`
		IncludeProjectUUID *bool             `json:"include_project_uuid"`
		IsShowDerive       *bool             `json:"is_show_derive"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	if len(payload.Query) == 0 || string(payload.Query) == "null" {
		return nil, runner.Errorf(runner.ErrorValidation, "query is required", nil)
	}
	body := map[string]any{"query": payload.Query}
	if payload.Sort != nil {
		body["sort"] = payload.Sort
	}
	if payload.GroupBy != nil {
		body["group_by"] = *payload.GroupBy
	}
	if payload.IncludeSubtasks != nil {
		body["include_subtasks"] = *payload.IncludeSubtasks
	}
	if payload.WithBoards != nil {
		body["with_boards"] = *payload.WithBoards
	}
	if payload.Boards != nil {
		body["boards"] = payload.Boards
	}
	if payload.IncludeStatusUUID != nil {
		body["include_status_uuid"] = *payload.IncludeStatusUUID
	}
	if payload.IncludeIssueType != nil {
		body["include_issue_type"] = *payload.IncludeIssueType
	}
	if payload.IncludeProjectUUID != nil {
		body["include_project_uuid"] = *payload.IncludeProjectUUID
	}
	if payload.IsShowDerive != nil {
		body["is_show_derive"] = *payload.IsShowDerive
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/filters/peek", nil, body))
}

func handleListSprints(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		ProjectUUID string `json:"project_uuid"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	project, err := pathParam16("project_uuid", payload.ProjectUUID)
	if err != nil {
		return nil, err
	}
	return client.Call(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/team/"+team+"/project/"+project+"/sprints", nil, nil))
}

func handleCreateSprint(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		ProjectUUID string            `json:"project_uuid"`
		Sprints     []json.RawMessage `json:"sprints"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	project, err := pathParam16("project_uuid", payload.ProjectUUID)
	if err != nil {
		return nil, err
	}
	if len(payload.Sprints) == 0 {
		return nil, runner.Errorf(runner.ErrorValidation, "sprints must not be empty", nil)
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/project/"+project+"/sprints/add", nil,
		map[string]any{"sprints": payload.Sprints}))
}

func handleBindTaskPage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		TaskUUID string            `json:"task_uuid"`
		Pages    []json.RawMessage `json:"pages"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	task, err := pathParam16("task_uuid", payload.TaskUUID)
	if err != nil {
		return nil, err
	}
	if len(payload.Pages) == 0 {
		return nil, runner.Errorf(runner.ErrorValidation, "pages must not be empty", nil)
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/task/"+task+"/bind_wiki_page", nil,
		map[string]any{"pages": payload.Pages}))
}

func handleUnbindTaskPage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var payload struct {
		teamScoped
		TaskUUID string          `json:"task_uuid"`
		Page     json.RawMessage `json:"page"`
	}
	if err := decodeInput(input, &payload); err != nil {
		return nil, err
	}
	team, err := payload.team()
	if err != nil {
		return nil, err
	}
	task, err := pathParam16("task_uuid", payload.TaskUUID)
	if err != nil {
		return nil, err
	}
	if len(payload.Page) == 0 || string(payload.Page) == "null" {
		return nil, runner.Errorf(runner.ErrorValidation, "page is required", nil)
	}
	return client.Call(ctx, credentials, request(credentials, "POST",
		ProjectPrefix+"/team/"+team+"/task/"+task+"/unbind_wiki_page", nil,
		map[string]any{"page": payload.Page}))
}
