package domain

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"
)

var (
	channelSlugPattern     = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
	memberIDPattern        = regexp.MustCompile(`^[a-z0-9]{6,16}$`)
	memberReferencePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,62}$`)
	rawScopeIDPattern      = regexp.MustCompile(`^[a-z0-9][a-z0-9_]{2,127}$`)
	teamTargetIDPattern    = regexp.MustCompile(`^team_[a-z0-9]{14}$`)
	threadShortIDPattern   = regexp.MustCompile(`^[0-9a-f]{6,8}$`)
)

// ErrInvalidArgument indicates the caller provided an invalid input string.
//
// The error message should be safe to return directly to clients.
var ErrInvalidArgument = errors.New("invalid argument")

type invalidArgumentError struct {
	message string
}

func (err invalidArgumentError) Error() string {
	return err.message
}

func (err invalidArgumentError) Unwrap() error {
	return ErrInvalidArgument
}

func invalidArgument(message string) error {
	trimmed := strings.TrimSpace(message)
	if trimmed == "" {
		return ErrInvalidArgument
	}
	return invalidArgumentError{message: trimmed}
}

// Channel is a workspace-wide append-only group conversation.
type Channel struct {
	ID             string
	WorkspaceID    string
	Slug           string
	DisplayName    string
	Description    string
	NextTaskNumber int64
	CreatedAt      time.Time
}

// ChannelMembership links a member to a channel.
type ChannelMembership struct {
	ChannelID string
	MemberID  string
	CreatedAt time.Time
}

// DMThread stores the canonical unordered pair for a direct message thread.
type DMThread struct {
	ID          string
	WorkspaceID string
	MemberAID   string
	MemberBID   string
	CreatedAt   time.Time
}

// ThreadReference identifies either a channel or DM target in protocol-compatible form.
type ThreadReference struct {
	Type         ThreadType
	ChannelSlug  string
	PeerMemberID string
	ShortID      string
}

// MessageTargetKind is the protocol-level kind of one author-facing message
// target. Syntax classification happens before any workspace lookup so every
// delivery entry point agrees that reserved entity prefixes such as #tsk_ and
// #team_ are not channel slugs.
type MessageTargetKind string

const (
	MessageTargetKindChannel     MessageTargetKind = "channel"
	MessageTargetKindReplyThread MessageTargetKind = "reply_thread"
	MessageTargetKindDM          MessageTargetKind = "dm"
	MessageTargetKindTask        MessageTargetKind = "task"
	MessageTargetKindTeam        MessageTargetKind = "team"
	MessageTargetKindWorkflow    MessageTargetKind = "workflow"
)

// MessageTargetReference is the shared typed union for daemon-compatible
// author-facing message targets. Exactly one variant is populated according to
// Kind. Resolution, access policy, DM creation, and idempotency remain the
// responsibility of the caller.
type MessageTargetReference struct {
	Kind           MessageTargetKind
	Thread         ThreadReference
	TaskScopeID    string
	Team           TeamTargetReference
	WorkflowPlanID string
}

// ResolvedMessageTarget is the canonical scope selected for an authored
// target. Kind describes the committed scope, not necessarily the authored
// pseudo-target (for example a Team target resolves to channel or reply_thread).
type ResolvedMessageTarget struct {
	Kind    MessageTargetKind
	ScopeID string
}

// TeamTargetReference identifies a Team-owned conversation target exposed by
// Team wakes. Team targets are protocol pseudo-targets: they must be resolved
// through workspace_teams.channel_id instead of the ordinary #channel slug
// grammar because entity ids contain underscores.
type TeamTargetReference struct {
	TeamID  string
	ShortID string
}

// ParseMessageTargetReference is the single syntax-classification entry point
// for daemon-compatible message targets. Reserved prefixes are claimed before
// the channel grammar so malformed task or Team targets fail as their declared
// kind instead of falling through to a misleading channel-slug error.
func ParseMessageTargetReference(value string) (MessageTargetReference, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return MessageTargetReference{}, invalidArgument("message target is required")
	}
	if strings.HasPrefix(trimmed, "#tsk_") {
		scopeID, _, err := parseRawScopeTarget(trimmed)
		if err != nil {
			return MessageTargetReference{}, err
		}
		return MessageTargetReference{Kind: MessageTargetKindTask, TaskScopeID: scopeID}, nil
	}
	if strings.HasPrefix(trimmed, "#team_") {
		team, _, err := parseTeamTargetReference(trimmed)
		if err != nil {
			return MessageTargetReference{}, err
		}
		return MessageTargetReference{Kind: MessageTargetKindTeam, Team: team}, nil
	}
	if strings.HasPrefix(trimmed, "workflow:") {
		planID := strings.TrimSpace(strings.TrimPrefix(trimmed, "workflow:"))
		if !rawScopeIDPattern.MatchString(planID) || strings.Contains(planID, ":") {
			return MessageTargetReference{}, invalidArgument(fmt.Sprintf("invalid workflow Plan id %q", planID))
		}
		return MessageTargetReference{
			Kind: MessageTargetKindWorkflow, WorkflowPlanID: planID,
		}, nil
	}
	thread, err := parseThreadReference(trimmed)
	if err != nil {
		return MessageTargetReference{}, err
	}
	kind := MessageTargetKindChannel
	switch {
	case strings.TrimSpace(thread.ShortID) != "":
		kind = MessageTargetKindReplyThread
	case thread.Type == ThreadTypeDM:
		kind = MessageTargetKindDM
	}
	return MessageTargetReference{Kind: kind, Thread: thread}, nil
}

// ParseThreadReference parses `#channel` and `dm:@member` labels used by the daemon protocol.
func ParseThreadReference(value string) (ThreadReference, error) {
	trimmed := strings.TrimSpace(value)
	return parseThreadReference(trimmed)
}

func parseThreadReference(trimmed string) (ThreadReference, error) {
	switch {
	case strings.HasPrefix(trimmed, "#"):
		raw := strings.TrimPrefix(trimmed, "#")
		slug, shortID, hasShortID := strings.Cut(raw, ":")
		if err := ValidateChannelSlug(slug); err != nil {
			return ThreadReference{}, err
		}
		if hasShortID {
			parsedShortID, err := parseThreadShortID(shortID)
			if err != nil {
				return ThreadReference{}, err
			}
			return ThreadReference{Type: ThreadTypeChannel, ChannelSlug: slug, ShortID: parsedShortID}, nil
		}
		return ThreadReference{Type: ThreadTypeChannel, ChannelSlug: slug}, nil
	case strings.HasPrefix(strings.ToLower(trimmed), "dm:@"):
		raw := trimmed[len("dm:@"):]
		peer, shortID, hasShortID := strings.Cut(raw, ":")
		if err := ValidateMemberReference(peer); err != nil {
			return ThreadReference{}, err
		}
		if hasShortID {
			parsedShortID, err := parseThreadShortID(shortID)
			if err != nil {
				return ThreadReference{}, err
			}
			return ThreadReference{Type: ThreadTypeDM, PeerMemberID: peer, ShortID: parsedShortID}, nil
		}
		return ThreadReference{Type: ThreadTypeDM, PeerMemberID: peer}, nil
	default:
		return ThreadReference{}, invalidArgument("invalid channel identifier")
	}
}

// ParseTeamTargetReference parses Team wake targets such as
// #team_xxxxxxxxxxxxxx and #team_xxxxxxxxxxxxxx:deadbeef. The optional short id
// follows reply-thread target semantics for Team channel messages.
func ParseTeamTargetReference(value string) (TeamTargetReference, bool, error) {
	trimmed := strings.TrimSpace(value)
	return parseTeamTargetReference(trimmed)
}

func parseTeamTargetReference(trimmed string) (TeamTargetReference, bool, error) {
	if !strings.HasPrefix(trimmed, "#team_") {
		return TeamTargetReference{}, false, nil
	}
	raw := strings.TrimPrefix(trimmed, "#")
	teamID, shortID, hasShortID := strings.Cut(raw, ":")
	if !teamTargetIDPattern.MatchString(teamID) {
		return TeamTargetReference{}, true, invalidArgument(fmt.Sprintf("invalid team target %q", raw))
	}
	reference := TeamTargetReference{TeamID: teamID}
	if hasShortID {
		parsedShortID, err := parseThreadShortID(shortID)
		if err != nil {
			return TeamTargetReference{}, true, err
		}
		reference.ShortID = parsedShortID
	}
	return reference, true, nil
}

// ParseRawScopeTarget parses daemon targets that directly name an existing
// task scope id, such as #tsk_xxx. It intentionally only claims the explicit
// task-scope prefix so Team pseudo-targets such as #team_xxx keep their own
// resolver path instead of being mistaken for raw scopes.
func ParseRawScopeTarget(value string) (string, bool, error) {
	trimmed := strings.TrimSpace(value)
	return parseRawScopeTarget(trimmed)
}

func parseRawScopeTarget(trimmed string) (string, bool, error) {
	if !strings.HasPrefix(trimmed, "#tsk_") {
		return "", false, nil
	}
	raw := strings.TrimPrefix(trimmed, "#")
	if raw == "tsk_" || strings.Contains(raw, ":") || !rawScopeIDPattern.MatchString(raw) {
		return "", true, invalidArgument(fmt.Sprintf("invalid scope id %q", raw))
	}
	return raw, true, nil
}

// String renders the reference back to the daemon-compatible identifier.
func (reference ThreadReference) String() string {
	suffix := ""
	if strings.TrimSpace(reference.ShortID) != "" {
		suffix = ":" + reference.ShortID
	}
	if reference.Type == ThreadTypeDM {
		return "dm:@" + reference.PeerMemberID + suffix
	}
	return "#" + reference.ChannelSlug + suffix
}

// String renders the Team target back to the daemon-compatible pseudo-target.
func (reference TeamTargetReference) String() string {
	suffix := ""
	if strings.TrimSpace(reference.ShortID) != "" {
		suffix = ":" + reference.ShortID
	}
	return "#" + reference.TeamID + suffix
}

// String renders the typed target back to its canonical daemon-compatible
// author-facing identifier.
func (reference MessageTargetReference) String() string {
	switch reference.Kind {
	case MessageTargetKindChannel, MessageTargetKindReplyThread, MessageTargetKindDM:
		return reference.Thread.String()
	case MessageTargetKindTask:
		return "#" + reference.TaskScopeID
	case MessageTargetKindTeam:
		return reference.Team.String()
	case MessageTargetKindWorkflow:
		return "workflow:" + reference.WorkflowPlanID
	default:
		return ""
	}
}

// ValidateChannelSlug checks the allowed channel slug format.
func ValidateChannelSlug(slug string) error {
	return validateSlug(slug, "channel")
}

// ValidateWorkspaceSlug checks the allowed workspace slug format.
func ValidateWorkspaceSlug(slug string) error {
	return validateSlug(slug, "workspace")
}

func validateSlug(slug string, kind string) error {
	trimmed := strings.TrimSpace(slug)
	if trimmed != slug {
		return invalidArgument(fmt.Sprintf("%s slug must not have surrounding whitespace", kind))
	}
	if !channelSlugPattern.MatchString(slug) {
		return invalidArgument(fmt.Sprintf("invalid %s slug %q", kind, slug))
	}
	return nil
}

// ValidateMemberID checks the stable protocol-facing member identifier format.
func ValidateMemberID(memberID string) error {
	trimmed := strings.TrimSpace(memberID)
	if trimmed != memberID {
		return invalidArgument("member id must not have surrounding whitespace")
	}
	if !memberIDPattern.MatchString(memberID) {
		return invalidArgument(fmt.Sprintf("invalid member id %q", memberID))
	}
	return nil
}

// ValidateMemberReference checks the protocol-facing DM peer token format.
//
// This accepts stable member ids and agent public handles. Resolution into an
// internal member id happens later against workspace state.
func ValidateMemberReference(reference string) error {
	trimmed := strings.TrimSpace(reference)
	if trimmed != reference {
		return invalidArgument("member reference must not have surrounding whitespace")
	}
	if !memberReferencePattern.MatchString(reference) {
		return invalidArgument(fmt.Sprintf("invalid member reference %q", reference))
	}
	return nil
}

func parseThreadShortID(value string) (string, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return "", invalidArgument("invalid thread identifier")
	}
	normalized := strings.ToLower(trimmed)
	if !threadShortIDPattern.MatchString(normalized) {
		return "", invalidArgument("invalid thread identifier")
	}
	return normalized, nil
}
