package service

import (
	"baron-sso-backend/internal/domain"
	"crypto/rand"
	"errors"
	"fmt"
	"math/big"
	"net/mail"
	"os"
	"sort"
	"strconv"
	"strings"
)

const (
	WorksmobileUserActionUpsert  = "UPSERT"
	WorksmobileUserActionSuspend = "SUSPEND"
)

type WorksmobileOrgUnitPayload struct {
	DomainID           int64  `json:"domainId"`
	OrgUnitName        string `json:"orgUnitName"`
	Email              string `json:"email,omitempty"`
	OrgUnitExternalKey string `json:"orgUnitExternalKey"`
	ParentOrgUnitID    string `json:"parentOrgUnitId,omitempty"`
	DisplayOrder       int    `json:"displayOrder"`
}

type WorksmobileUserPayload struct {
	DomainID        int64                         `json:"domainId"`
	Email           string                        `json:"email"`
	UserExternalKey string                        `json:"userExternalKey"`
	UserName        WorksmobileUserName           `json:"userName"`
	CellPhone       string                        `json:"cellPhone,omitempty"`
	EmployeeNumber  string                        `json:"employeeNumber,omitempty"`
	PrivateEmail    string                        `json:"privateEmail,omitempty"`
	AliasEmails     []string                      `json:"aliasEmails,omitempty"`
	Locale          string                        `json:"locale,omitempty"`
	PasswordConfig  WorksmobilePasswordConfig     `json:"passwordConfig"`
	Task            string                        `json:"task,omitempty"`
	Organizations   []WorksmobileUserOrganization `json:"organizations,omitempty"`
}

type WorksmobileUserName struct {
	LastName string `json:"lastName,omitempty"`
}

type WorksmobilePasswordConfig struct {
	PasswordCreationType string `json:"passwordCreationType"`
	Password             string `json:"password"`
}

type WorksmobileUserOrganization struct {
	DomainID int64                    `json:"domainId,omitempty"`
	Primary  bool                     `json:"primary,omitempty"`
	OrgUnits []WorksmobileUserOrgUnit `json:"orgUnits"`
}

type WorksmobileUserOrgUnit struct {
	OrgUnitID  string `json:"orgUnitId"`
	Primary    bool   `json:"primary,omitempty"`
	PositionID string `json:"positionId,omitempty"`
	IsManager  *bool  `json:"isManager,omitempty"`
}

func BuildWorksmobileOrgUnitPayload(tenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) {
	return BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, tenant, rootConfig, displayOrder)
}

func BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant domain.Tenant, domainTenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) {
	if err := ValidateWorksmobileExternalKey(tenant.ID); err != nil {
		return WorksmobileOrgUnitPayload{}, err
	}
	if displayOrder < 1 {
		displayOrder = 1
	}
	domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
	if err != nil {
		return WorksmobileOrgUnitPayload{}, err
	}
	payload := WorksmobileOrgUnitPayload{
		DomainID:           domainID,
		OrgUnitName:        strings.TrimSpace(tenant.Name),
		Email:              buildWorksmobileOrgUnitEmail(tenant, domainTenant),
		OrgUnitExternalKey: tenant.ID,
		DisplayOrder:       displayOrder,
	}
	if tenant.ParentID != nil && *tenant.ParentID != "" {
		if err := ValidateWorksmobileExternalKey(*tenant.ParentID); err != nil {
			return WorksmobileOrgUnitPayload{}, err
		}
		payload.ParentOrgUnitID = "externalKey:" + *tenant.ParentID
	}
	return payload, nil
}

func buildWorksmobileOrgUnitEmail(tenant domain.Tenant, domainTenant domain.Tenant) string {
	slug := strings.ToLower(strings.TrimSpace(tenant.Slug))
	if slug == "" {
		return ""
	}
	if domainName := worksmobileTenantMailDomain(domainTenant); domainName != "" {
		return slug + "@" + domainName
	}
	for _, candidate := range append([]domain.TenantDomain{}, domainTenant.Domains...) {
		domainName := strings.ToLower(strings.TrimSpace(candidate.Domain))
		if domainName != "" {
			return slug + "@" + domainName
		}
	}
	for _, candidate := range tenant.Domains {
		domainName := strings.ToLower(strings.TrimSpace(candidate.Domain))
		if domainName != "" {
			return slug + "@" + domainName
		}
	}
	return ""
}

func worksmobileTenantMailDomain(tenant domain.Tenant) string {
	envKey := strings.TrimSuffix(worksmobileTenantDomainIDEnvKey(tenant), "_DOMAIN_ID")
	if domainName := strings.ToLower(strings.TrimSpace(os.Getenv(envKey + "_MAIL_DOMAIN"))); domainName != "" {
		return domainName
	}
	switch envKey {
	case "SAMAN":
		return "samaneng.com"
	case "HANMAC":
		return "hanmaceng.co.kr"
	case "GPDTDC":
		return "baroncs.co.kr"
	case "BARONGROUP":
		return "brsw.kr"
	default:
		return ""
	}
}

func BuildWorksmobileUserPayload(user domain.User, tenant domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
	return BuildWorksmobileUserPayloadForDomainTenant(user, tenant, tenant, rootConfig)
}

func BuildWorksmobileUserPayloadForDomainTenant(user domain.User, tenant domain.Tenant, _ domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
	return BuildWorksmobileUserPayloadForDomainTenants(user, tenant, map[string]domain.Tenant{tenant.ID: tenant}, rootConfig)
}

func BuildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
	if err := ValidateWorksmobileExternalKey(user.ID); err != nil {
		return WorksmobileUserPayload{}, err
	}
	if tenant.ID == "" {
		return WorksmobileUserPayload{}, errors.New("tenant is required")
	}
	if tenantByID == nil {
		tenantByID = map[string]domain.Tenant{}
	}
	tenantByID[tenant.ID] = tenant
	domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
	domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
	if err != nil {
		return WorksmobileUserPayload{}, err
	}
	employeeNumber := metadataString(user.Metadata, "employee_id", "employeeNumber", "employee_number")
	organizations, task, err := buildWorksmobileUserOrganizations(user, tenant, tenantByID, rootConfig)
	if err != nil {
		return WorksmobileUserPayload{}, err
	}
	if task == "" {
		task = strings.TrimSpace(user.JobTitle)
	}
	payload := WorksmobileUserPayload{
		DomainID:        domainID,
		Email:           strings.TrimSpace(user.Email),
		UserExternalKey: user.ID,
		UserName:        WorksmobileUserName{LastName: strings.TrimSpace(user.Name)},
		CellPhone:       strings.TrimSpace(user.Phone),
		EmployeeNumber:  employeeNumber,
		Locale:          "ko_KR",
		PasswordConfig: WorksmobilePasswordConfig{
			PasswordCreationType: "ADMIN",
			Password:             GenerateWorksmobileInitialPassword(),
		},
		Task:          task,
		Organizations: organizations,
	}
	payload.AliasEmails = BuildWorksmobileAliasEmails(user, tenant)
	return payload, nil
}

type worksmobileAppointment struct {
	TenantID   string
	IsPrimary  bool
	IsOwner    bool
	HasOwner   bool
	JobTitle   string
	PositionID string
}

func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) ([]WorksmobileUserOrganization, string, error) {
	appointments := worksmobileAppointmentsFromMetadata(user.Metadata)
	if len(appointments) == 0 {
		appointments = []worksmobileAppointment{{TenantID: tenant.ID, IsPrimary: true}}
	}
	primaryTenantID := metadataString(user.Metadata, "primaryTenantId", "primary_tenant_id")
	if primaryTenantID == "" && user.TenantID != nil {
		primaryTenantID = *user.TenantID
	}
	hasPrimary := false
	for i := range appointments {
		if appointments[i].TenantID == primaryTenantID || appointments[i].IsPrimary {
			appointments[i].IsPrimary = true
			hasPrimary = true
			break
		}
	}
	if !hasPrimary {
		for i := range appointments {
			if appointments[i].TenantID == tenant.ID {
				appointments[i].IsPrimary = true
				break
			}
		}
	}

	organizations := make([]WorksmobileUserOrganization, 0, len(appointments))
	seen := map[string]bool{}
	task := ""
	for _, appointment := range appointments {
		if appointment.TenantID == "" || seen[appointment.TenantID] {
			continue
		}
		appointmentTenant, ok := tenantByID[appointment.TenantID]
		if !ok {
			continue
		}
		if err := ValidateWorksmobileExternalKey(appointmentTenant.ID); err != nil {
			return nil, "", err
		}
		domainTenant := worksmobileDomainClassificationTenant(appointmentTenant, tenantByID)
		domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
		if err != nil {
			return nil, "", err
		}
		orgUnit := WorksmobileUserOrgUnit{
			OrgUnitID:  "externalKey:" + appointmentTenant.ID,
			Primary:    appointment.IsPrimary,
			PositionID: appointment.PositionID,
		}
		if appointment.HasOwner {
			isManager := appointment.IsOwner
			orgUnit.IsManager = &isManager
		}
		organizations = append(organizations, WorksmobileUserOrganization{
			DomainID: domainID,
			Primary:  appointment.IsPrimary,
			OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
		})
		if appointment.IsPrimary && strings.TrimSpace(appointment.JobTitle) != "" {
			task = strings.TrimSpace(appointment.JobTitle)
		}
		seen[appointment.TenantID] = true
	}
	if len(organizations) == 0 {
		return nil, "", errors.New("no valid worksmobile organization")
	}
	sortWorksmobileOrganizations(organizations)
	return organizations, task, nil
}

func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileAppointment {
	rawAppointments, ok := metadata["additionalAppointments"].([]any)
	if !ok {
		return nil
	}
	appointments := make([]worksmobileAppointment, 0, len(rawAppointments))
	for _, raw := range rawAppointments {
		item, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		appointment := worksmobileAppointment{
			TenantID:   metadataString(domain.JSONMap(item), "tenantId", "tenant_id"),
			IsPrimary:  metadataBool(domain.JSONMap(item), "isPrimary", "primary"),
			JobTitle:   metadataString(domain.JSONMap(item), "jobTitle", "job_title", "task"),
			PositionID: metadataString(domain.JSONMap(item), "worksmobilePositionId", "positionId", "position_id"),
		}
		if isOwner, ok := metadataOptionalBool(domain.JSONMap(item), "isOwner", "isManager"); ok {
			appointment.IsOwner = isOwner
			appointment.HasOwner = true
		}
		appointments = append(appointments, appointment)
	}
	return appointments
}

func sortWorksmobileOrganizations(organizations []WorksmobileUserOrganization) {
	sort.SliceStable(organizations, func(i, j int) bool {
		if organizations[i].Primary != organizations[j].Primary {
			return organizations[i].Primary
		}
		left := ""
		right := ""
		if len(organizations[i].OrgUnits) > 0 {
			left = organizations[i].OrgUnits[0].OrgUnitID
		}
		if len(organizations[j].OrgUnits) > 0 {
			right = organizations[j].OrgUnits[0].OrgUnitID
		}
		return left < right
	})
}

func BuildWorksmobileAliasEmails(user domain.User, tenant domain.Tenant) []string {
	candidates := metadataStringList(user.Metadata, "aliasEmails", "alias_emails", "worksmobileAliasEmails")
	employeeNumber := metadataString(user.Metadata, "employee_id", "employeeNumber", "employee_number")
	if isHanmacWorksmobileTenant(tenant) && employeeNumber != "" {
		candidates = append(candidates, employeeNumber+"@hanmaceng.co.kr")
	}
	return normalizeWorksmobileAliasEmails(user.Email, candidates)
}

func normalizeWorksmobileAliasEmails(primaryEmail string, candidates []string) []string {
	result := make([]string, 0, len(candidates))
	seen := map[string]bool{}
	primary := strings.ToLower(strings.TrimSpace(primaryEmail))
	for _, candidate := range candidates {
		normalized := strings.ToLower(strings.TrimSpace(candidate))
		if normalized == "" || normalized == primary || seen[normalized] {
			continue
		}
		if _, err := mail.ParseAddress(normalized); err != nil {
			continue
		}
		seen[normalized] = true
		result = append(result, normalized)
	}
	return result
}

func ValidateWorksmobileAliasLocalParts(primaryEmail string, aliasEmails []string, existingLocalParts map[string]string) error {
	seen := map[string]string{}
	primaryLocalPart, err := domain.ExtractNormalizedEmailLocalPart(primaryEmail)
	if err != nil {
		return err
	}
	seen[primaryLocalPart] = primaryEmail

	for _, aliasEmail := range aliasEmails {
		localPart, err := domain.ExtractNormalizedEmailLocalPart(aliasEmail)
		if err != nil {
			return err
		}
		if previous, ok := seen[localPart]; ok {
			return fmt.Errorf("worksmobile alias local-part duplicates %s: %s and %s", localPart, previous, aliasEmail)
		}
		if owner, ok := existingLocalParts[localPart]; ok {
			return fmt.Errorf("worksmobile alias local-part %s는 이미 사용 중입니다: %s", localPart, owner)
		}
		seen[localPart] = aliasEmail
	}
	return nil
}

func GenerateWorksmobileInitialPassword() string {
	digits := "0123456789"
	letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	symbols := "!@#$%^&*()-_=+[]{}"
	all := digits + letters + symbols

	password := []byte{
		randomChar(digits),
		randomChar(letters),
		randomChar(symbols),
	}
	for len(password) < 16 {
		password = append(password, randomChar(all))
	}
	shuffleBytes(password)
	return string(password)
}

func randomChar(chars string) byte {
	if chars == "" {
		return 'x'
	}
	index, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
	if err != nil {
		return chars[0]
	}
	return chars[index.Int64()]
}

func shuffleBytes(values []byte) {
	for i := len(values) - 1; i > 0; i-- {
		j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
		if err != nil {
			continue
		}
		values[i], values[j.Int64()] = values[j.Int64()], values[i]
	}
}

func WorksmobileUserStatusAction(status string) string {
	normalized := domain.NormalizeUserStatus(status)
	if domain.IsWorksDeprovisionUserStatus(normalized) {
		return domain.WorksmobileActionDelete
	}
	switch normalized {
	case domain.UserStatusSuspended:
		return WorksmobileUserActionSuspend
	default:
		return WorksmobileUserActionUpsert
	}
}

func ValidateWorksmobileExternalKey(value string) error {
	value = strings.TrimSpace(value)
	if value == "" {
		return errors.New("external key is required")
	}
	if strings.ContainsAny(value, `%\#/?`) {
		return fmt.Errorf("external key contains unsupported character: %s", value)
	}
	return nil
}

func ResolveWorksmobileDomainIDFromTenant(tenant domain.Tenant, _ domain.JSONMap) (int64, error) {
	envKey := worksmobileTenantDomainIDEnvKey(tenant)
	if domainID, ok := worksmobileDomainIDFromEnv(envKey); ok {
		return domainID, nil
	}
	return 0, fmt.Errorf("worksmobile domain id env is missing for tenant: %s", envKey)
}

func worksmobileTenantDomainIDEnvKey(tenant domain.Tenant) string {
	if tenantHasDomain(tenant, "samaneng.com") || tenantMatchesAny(tenant, "saman", "삼안") {
		return "SAMAN_DOMAIN_ID"
	}
	if isHanmacWorksmobileTenant(tenant) {
		return "HANMAC_DOMAIN_ID"
	}
	if tenantMatchesAny(tenant, "gpdtdc", "총괄", "기술개발센터", "기술개발") {
		return "GPDTDC_DOMAIN_ID"
	}
	return "BARONGROUP_DOMAIN_ID"
}

func worksmobileDomainIDFromEnv(key string) (int64, bool) {
	if key == "" {
		return 0, false
	}
	id, ok := parseDomainID(os.Getenv(key))
	return id, ok
}

type worksmobileDomainEnvMapping struct {
	Key   string
	Label string
}

func worksmobileDomainEnvMappings() []worksmobileDomainEnvMapping {
	return []worksmobileDomainEnvMapping{
		{Key: "SAMAN_DOMAIN_ID", Label: "삼안"},
		{Key: "HANMAC_DOMAIN_ID", Label: "한맥기술"},
		{Key: "GPDTDC_DOMAIN_ID", Label: "총괄기획&기술개발센터"},
		{Key: "BARONGROUP_DOMAIN_ID", Label: "바론그룹"},
	}
}

func WorksmobileDomainIDsFromEnv() []int64 {
	mappings := worksmobileDomainEnvMappings()
	result := make([]int64, 0, len(mappings))
	seen := map[int64]bool{}
	for _, mapping := range mappings {
		if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && !seen[id] {
			seen[id] = true
			result = append(result, id)
		}
	}
	return result
}

func WorksmobileDomainLabelForID(domainID int64) string {
	for _, mapping := range worksmobileDomainEnvMappings() {
		if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && id == domainID {
			return mapping.Label
		}
	}
	return ""
}

func isHanmacWorksmobileTenant(tenant domain.Tenant) bool {
	return tenantHasDomain(tenant, "hanmaceng.co.kr") || tenantMatchesAny(tenant, "hanmac", "한맥")
}

func tenantHasDomain(tenant domain.Tenant, domainName string) bool {
	domainName = strings.ToLower(strings.TrimSpace(domainName))
	for _, d := range tenant.Domains {
		if strings.EqualFold(strings.TrimSpace(d.Domain), domainName) {
			return true
		}
	}
	return false
}

func tenantMatchesAny(tenant domain.Tenant, needles ...string) bool {
	haystack := strings.ToLower(strings.TrimSpace(tenant.Slug + " " + tenant.Name))
	for _, needle := range needles {
		if strings.Contains(haystack, strings.ToLower(strings.TrimSpace(needle))) {
			return true
		}
	}
	return false
}

func WorksmobileEnabled(rootConfig domain.JSONMap) bool {
	rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any)
	if !ok {
		if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok {
			rawWorksmobile = map[string]any(raw)
		} else {
			return false
		}
	}
	enabled, _ := rawWorksmobile["enabled"].(bool)
	return enabled
}

func WorksmobileDomainMappings(rootConfig domain.JSONMap) map[string]int64 {
	result := map[string]int64{}
	rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any)
	if !ok {
		if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok {
			rawWorksmobile = map[string]any(raw)
		} else {
			return result
		}
	}
	rawMappings, ok := rawWorksmobile["domainMappings"].(map[string]any)
	if !ok {
		if raw, ok := rawWorksmobile["domainMappings"].(domain.JSONMap); ok {
			rawMappings = map[string]any(raw)
		} else {
			return result
		}
	}
	for key, raw := range rawMappings {
		if id, ok := parseDomainID(raw); ok {
			result[strings.ToLower(strings.TrimSpace(key))] = id
		}
	}
	return result
}

func parseDomainID(raw any) (int64, bool) {
	switch value := raw.(type) {
	case int:
		return int64(value), value > 0
	case int64:
		return value, value > 0
	case float64:
		id := int64(value)
		return id, id > 0
	case string:
		id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
		return id, err == nil && id > 0
	default:
		return 0, false
	}
}

func metadataString(metadata domain.JSONMap, keys ...string) string {
	for _, key := range keys {
		if value, ok := metadata[key]; ok {
			switch v := value.(type) {
			case string:
				return strings.TrimSpace(v)
			default:
				return strings.TrimSpace(fmt.Sprint(v))
			}
		}
	}
	return ""
}

func metadataBool(metadata domain.JSONMap, keys ...string) bool {
	value, _ := metadataOptionalBool(metadata, keys...)
	return value
}

func metadataOptionalBool(metadata domain.JSONMap, keys ...string) (bool, bool) {
	for _, key := range keys {
		value, ok := metadata[key]
		if !ok {
			continue
		}
		switch v := value.(type) {
		case bool:
			return v, true
		case string:
			normalized := strings.ToLower(strings.TrimSpace(v))
			if normalized == "true" || normalized == "1" || normalized == "yes" {
				return true, true
			}
			if normalized == "false" || normalized == "0" || normalized == "no" {
				return false, true
			}
		case int:
			return v != 0, true
		case float64:
			return v != 0, true
		}
	}
	return false, false
}

func metadataStringList(metadata domain.JSONMap, keys ...string) []string {
	for _, key := range keys {
		value, ok := metadata[key]
		if !ok {
			continue
		}
		switch v := value.(type) {
		case []string:
			return splitWorksmobileAliasValues(v)
		case []any:
			values := make([]string, 0, len(v))
			for _, item := range v {
				values = append(values, strings.TrimSpace(fmt.Sprint(item)))
			}
			return splitWorksmobileAliasValues(values)
		case string:
			return splitWorksmobileAliasValues([]string{v})
		default:
			return splitWorksmobileAliasValues([]string{fmt.Sprint(v)})
		}
	}
	return nil
}

func splitWorksmobileAliasValues(values []string) []string {
	result := make([]string, 0, len(values))
	for _, value := range values {
		fields := strings.FieldsFunc(value, func(r rune) bool {
			return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t'
		})
		for _, field := range fields {
			if trimmed := strings.TrimSpace(field); trimmed != "" {
				result = append(result, trimmed)
			}
		}
	}
	return result
}
