package externaladmin

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"hyapp-admin-server/internal/cache"
)

const (
	SessionCookieName = "hyapp_external_session"
	CSRFCookieName    = "hyapp_external_csrf"
	CSRFHeaderName    = "X-CSRF-Token"
	CookiePath        = "/api/v1/external"
)

var (
	ErrInvalidCredentials = errors.New("invalid external admin credentials")
	ErrAccountConflict    = errors.New("external admin account conflict")
	ErrAccountNotFound    = errors.New("external admin account not found")
	ErrTargetNotFound     = errors.New("linked app user not found")
	ErrInvalidInput       = errors.New("invalid external admin input")
	ErrInvalidPermissions = errors.New("invalid external admin permissions")
	ErrPermissionConflict = errors.New("external admin permission revision conflict")
	ErrParentConflict     = errors.New("external admin parent revision conflict")
	ErrInvalidHierarchy   = errors.New("invalid external admin hierarchy")
	ErrInvalidSession     = errors.New("invalid external admin session")
	ErrPasswordReused     = errors.New("new external admin password matches current password")
	ErrPasswordBlank      = errors.New("external admin password cannot be only whitespace")
	ErrLoginRateLimited   = errors.New("external admin login rate limited")
	ErrLoginLimiterFailed = errors.New("external admin login rate limiter unavailable")
)

const externalOperatorNamespace uint64 = 1 << 62

// Config contains only external-portal controls. Cookie transport attributes are inherited
// from the already validated admin cookie settings so local/prod deployments stay consistent.
type Config struct {
	SessionTTL       time.Duration
	MaxLoginFailures uint
	LockDuration     time.Duration
	CookieSecure     bool
	CookieSameSite   string
	LoginRateLimit   LoginRateLimitConfig
}

type LoginRateLimitConfig struct {
	Window        time.Duration
	SystemLimit   uint64
	AppLimit      uint64
	IPLimit       uint64
	IdentityLimit uint64
}

type FixedWindowLimiter interface {
	ConsumeFixedWindow(context.Context, time.Duration, []cache.FixedWindowRule) (cache.FixedWindowDecision, error)
}

type LoginRateLimitError struct {
	RetryAfter time.Duration
}

func (err *LoginRateLimitError) Error() string {
	return ErrLoginRateLimited.Error()
}

func (err *LoginRateLimitError) Unwrap() error {
	return ErrLoginRateLimited
}

type App struct {
	AppCode string `json:"appCode"`
	AppName string `json:"appName"`
	LogoURL string `json:"logoUrl"`
}

type LinkedUser struct {
	UserID               int64  `json:"userId,string"`
	DisplayUserID        string `json:"displayUserId"`
	DefaultDisplayUserID string `json:"defaultDisplayUserId"`
	PrettyDisplayUserID  string `json:"prettyDisplayUserId"`
	PrettyID             string `json:"prettyId"`
	Username             string `json:"username"`
	Avatar               string `json:"avatar"`
	Status               string `json:"status"`
}

type AccountDTO struct {
	ID                 uint64            `json:"id"`
	AppCode            string            `json:"appCode"`
	Username           string            `json:"username"`
	Status             string            `json:"status"`
	LinkedUser         LinkedUser        `json:"linkedUser"`
	Permissions        []string          `json:"permissions"`
	PermissionRevision uint64            `json:"permissionRevision"`
	IdentityType       string            `json:"identityType"`
	ParentAccountID    *uint64           `json:"parentAccountId,omitempty"`
	Parent             *AccountReference `json:"parent,omitempty"`
	CreatedByAdminID   uint              `json:"createdByAdminId"`
	CreatedAtMS        int64             `json:"createdAtMs"`
	UpdatedAtMS        int64             `json:"updatedAtMs"`
	LastLoginAtMS      *int64            `json:"lastLoginAtMs"`
}

// AccountReference is the bounded parent/descendant projection used by selectors
// and hierarchy lists. It deliberately excludes permissions and credentials.
type AccountReference struct {
	ID           uint64     `json:"id"`
	Username     string     `json:"username"`
	IdentityType string     `json:"identityType"`
	Status       string     `json:"status"`
	LinkedUser   LinkedUser `json:"linkedUser"`
}

type AccountSummary struct {
	ID              uint64  `json:"id"`
	Username        string  `json:"username"`
	Status          string  `json:"status"`
	IdentityType    string  `json:"identityType"`
	ParentAccountID *uint64 `json:"parentAccountId,omitempty"`
}

type SessionView struct {
	User                    LinkedUser     `json:"user"`
	Account                 AccountSummary `json:"account"`
	App                     App            `json:"app"`
	AppCode                 string         `json:"appCode"`
	Permissions             []string       `json:"permissions"`
	Capabilities            []string       `json:"capabilities"`
	IdentityType            string         `json:"identityType"`
	RegionID                int64          `json:"regionId"`
	ExternalSuperAdminCount int64          `json:"externalSuperAdminCount"`
	PasswordChangeRequired  bool           `json:"passwordChangeRequired"`
	CSRFToken               string         `json:"csrfToken,omitempty"`
}

type LoginInput struct {
	Username  string
	Password  string
	IP        string
	UserAgent string
}

type LoginResult struct {
	SessionToken string
	CSRFToken    string
	View         SessionView
}

type ListInput struct {
	AppCode  string
	Page     int
	PageSize int
	Keyword  string
	Status   string
}

type ListResult struct {
	Items    []AccountDTO `json:"items"`
	Page     int          `json:"page"`
	PageSize int          `json:"pageSize"`
	Total    int64        `json:"total"`
}

type CreateInput struct {
	AppCode          string
	TargetUserID     string
	Username         string
	Password         string
	Permissions      []string
	PermissionsSet   bool
	IdentityType     string
	ParentAccountID  *uint64
	CreatedByAdminID uint
}

type UpdateParentInput struct {
	ParentAccountID  *uint64
	ExpectedRevision uint64
}

type ParentUpdateResult struct {
	Account         AccountDTO `json:"account"`
	Changed         bool       `json:"changed"`
	SessionsRevoked bool       `json:"sessionsRevoked"`
}

type ParentCandidateInput struct {
	AppCode        string
	IdentityType   string
	TargetUserID   string
	ChildAccountID uint64
	Keyword        string
}

type ParentCandidateResult struct {
	Items     []AccountReference `json:"items"`
	Truncated bool               `json:"truncated"`
}

type ChangePasswordInput struct {
	CurrentPassword string
	NewPassword     string
}

type SessionPrincipal struct {
	SessionID              uint64
	AccountID              uint64
	OperatorID             uint
	AppCode                string
	LinkedAppUserID        int64
	IdentityType           string
	RegionID               int64
	ScopeOwnerUserIDs      []int64
	Username               string
	Permissions            []string
	PasswordChangeRequired bool
	CSRFTokenHash          string
	ExpiresAtMS            int64
}

// MyTeamAgency is intentionally a narrow projection of the owner service's active
// manager-team relation. IDs are encoded as strings so browser clients cannot lose
// precision when HYApp's int64 identifiers exceed JavaScript's safe integer range.
type MyTeamAgency struct {
	AgencyID       int64  `json:"agencyId,string"`
	OwnerUserID    int64  `json:"ownerUserId,string"`
	ParentBDUserID int64  `json:"parentBdUserId,string"`
	Status         string `json:"status"`
}

type MyTeamAgencyPage struct {
	Items          []MyTeamAgency `json:"items"`
	Page           int            `json:"page"`
	PageSize       int            `json:"pageSize"`
	Total          int            `json:"total"`
	TotalBDLeaders int32          `json:"totalBdLeaders"`
	TotalBDs       int32          `json:"totalBds"`
	Truncated      bool           `json:"truncated"`
}

func operatorIDForAccount(accountID uint64) (uint, error) {
	if accountID == 0 || accountID >= externalOperatorNamespace {
		return 0, ErrInvalidSession
	}
	// Reused business handlers accept only a positive admin/operator integer. A high-bit namespace
	// prevents external account #1 from being recorded as main admin #1 while staying <= MaxInt64
	// for signed downstream protos and positive for UNSIGNED audit columns.
	return uint(externalOperatorNamespace | accountID), nil
}

func operatorUsername(appCode string, username string) string {
	return truncateText("external:"+normalizeAppCode(appCode)+":"+normalizeUsername(username), 64)
}

// FlexibleString accepts either a JSON string or number. Short IDs must remain textual in
// storage, but accepting numeric JSON prevents clients from silently losing older contracts.
type FlexibleString string

func (value *FlexibleString) UnmarshalJSON(body []byte) error {
	body = bytes.TrimSpace(body)
	if len(body) == 0 || bytes.Equal(body, []byte("null")) {
		*value = ""
		return nil
	}
	if body[0] == '"' {
		var text string
		if err := json.Unmarshal(body, &text); err != nil {
			return err
		}
		*value = FlexibleString(strings.TrimSpace(text))
		return nil
	}
	var number json.Number
	if err := json.Unmarshal(body, &number); err != nil {
		return fmt.Errorf("targetUserId must be a string or integer: %w", err)
	}
	if _, err := strconv.ParseInt(number.String(), 10, 64); err != nil {
		return fmt.Errorf("targetUserId must be an integer: %w", err)
	}
	*value = FlexibleString(number.String())
	return nil
}

func DefaultPermissionSnapshot() []string {
	permissions := make([]string, 0, len(permissionCatalog))
	for _, item := range permissionCatalog {
		if item.DefaultGranted {
			permissions = append(permissions, item.Code)
		}
	}
	return normalizePermissions(permissions)
}

func encodePermissions(permissions []string) (string, error) {
	normalized := normalizePermissions(permissions)
	body, err := json.Marshal(normalized)
	return string(body), err
}

func decodePermissions(raw string) ([]string, error) {
	permissions := []string{}
	if err := json.Unmarshal([]byte(raw), &permissions); err != nil {
		return nil, err
	}
	return effectivePermissions(permissions), nil
}

func normalizePermissions(permissions []string) []string {
	seen := make(map[string]struct{}, len(permissions))
	out := make([]string, 0, len(permissions))
	for _, permission := range permissions {
		permission = strings.TrimSpace(permission)
		if permission == "" {
			continue
		}
		if _, ok := seen[permission]; ok {
			continue
		}
		seen[permission] = struct{}{}
		out = append(out, permission)
	}
	sort.Strings(out)
	return out
}

func capabilitiesFor(permissions []string) []string {
	permissions = effectivePermissions(permissions)
	permissionSet := make(map[string]struct{}, len(permissions))
	for _, permission := range permissions {
		permissionSet[permission] = struct{}{}
	}
	aliases := permissionCapabilityAliases()
	seen := map[string]struct{}{}
	capabilities := []string{}
	for permission := range permissionSet {
		for _, capability := range aliases[permission] {
			if _, ok := seen[capability]; ok {
				continue
			}
			seen[capability] = struct{}{}
			capabilities = append(capabilities, capability)
		}
	}
	sort.Strings(capabilities)
	return capabilities
}
