package rbac

import (
	"context"
	"errors"
	"strings"

	"security-center-backend/internal/model"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/service/directory"
)

var ErrRoleNotFound = errors.New("role not found")

type Service struct {
	users       repository.UserRepository
	roles       repository.RoleRepository
	perms       repository.PermissionRepository
	employees   directory.EmployeeDirectory
	departments directory.DepartmentDirectory
}

func NewService(
	userRepo repository.UserRepository,
	roleRepo repository.RoleRepository,
	permRepo repository.PermissionRepository,
	employeeRepo directory.EmployeeDirectory,
	departmentRepo directory.DepartmentDirectory,
) *Service {
	return &Service{
		users:       userRepo,
		roles:       roleRepo,
		perms:       permRepo,
		employees:   employeeRepo,
		departments: departmentRepo,
	}
}

type UserListResult struct {
	Items []model.User
	Total int64
}

type ListUsersInput struct {
	Page     int
	PageSize int
	Query    string
	Status   model.UserStatus
	RoleID   uint64
}

func (s *Service) ListUsers(ctx context.Context, input ListUsersInput) (*UserListResult, error) {
	if s.users == nil {
		return &UserListResult{}, nil
	}
	items, total, err := s.users.List(ctx, repository.UserFilter{
		Query:  input.Query,
		Status: input.Status,
		RoleID: input.RoleID,
	}, input.Page, input.PageSize)
	if err != nil {
		return nil, err
	}
	s.enrichUsersWithDepartmentNames(ctx, items)
	return &UserListResult{Items: items, Total: total}, nil
}

func (s *Service) GetUser(ctx context.Context, id uint64) (*model.User, error) {
	if s.users == nil {
		return nil, errors.New("user repository not configured")
	}
	user, err := s.users.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}
	enriched := []model.User{*user}
	s.enrichUsersWithDepartmentNames(ctx, enriched)
	*user = enriched[0]
	return user, nil
}

func (s *Service) enrichUsersWithDepartmentNames(ctx context.Context, users []model.User) {
	if len(users) == 0 || s.departments == nil {
		return
	}
	deptSet := make(map[string]struct{})
	for _, user := range users {
		if user.Employee == nil || user.Employee.DepartmentID == "" {
			continue
		}
		deptSet[user.Employee.DepartmentID] = struct{}{}
	}
	if len(deptSet) == 0 {
		return
	}
	deptIDs := make([]string, 0, len(deptSet))
	for deptID := range deptSet {
		deptIDs = append(deptIDs, deptID)
	}
	departments, err := s.departments.ListByLarkIDs(ctx, deptIDs)
	if err != nil {
		return
	}
	nameByID := make(map[string]string, len(departments))
	for _, dept := range departments {
		if dept.LarkDepartmentID == "" || dept.Name == "" {
			continue
		}
		nameByID[dept.LarkDepartmentID] = dept.Name
	}
	for i := range users {
		if users[i].Employee == nil || users[i].Employee.DepartmentID == "" {
			continue
		}
		if name := nameByID[users[i].Employee.DepartmentID]; name != "" {
			users[i].Employee.DepartmentName = name
		}
	}
}

type UpdateUserInput struct {
	Name          string
	Status        model.UserStatus
	DefaultRoleID *uint64
}

func (s *Service) UpdateUser(ctx context.Context, id uint64, input UpdateUserInput) (*model.User, error) {
	if s.users == nil {
		return nil, errors.New("user repository not configured")
	}
	user, err := s.users.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if input.Name != "" {
		user.Name = input.Name
	}
	if input.Status != "" {
		user.Status = input.Status
	}
	if input.DefaultRoleID != nil {
		roles, _ := s.users.GetUserRoles(ctx, id)
		if !containsRole(roles, *input.DefaultRoleID) {
			current := extractRoleIDs(roles)
			current = append(current, *input.DefaultRoleID)
			if err := s.users.AssignRoles(ctx, id, current); err != nil {
				return nil, err
			}
			roles, _ = s.users.GetUserRoles(ctx, id)
		}
		user.DefaultRoleID = input.DefaultRoleID
		user.Roles = roles
		if err := s.users.UpdateDefaultRole(ctx, id, input.DefaultRoleID); err != nil {
			return nil, err
		}
	}
	if err := s.users.CreateOrUpdate(ctx, user); err != nil {
		return nil, err
	}
	return user, nil
}

type AssignRolesInput struct {
	RoleIDs       []uint64
	DefaultRoleID *uint64
}

func (s *Service) AssignRoles(ctx context.Context, id uint64, input AssignRolesInput) error {
	if s.users == nil {
		return errors.New("user repository not configured")
	}
	if err := s.users.AssignRoles(ctx, id, input.RoleIDs); err != nil {
		return err
	}
	if input.DefaultRoleID != nil {
		if !containsID(input.RoleIDs, *input.DefaultRoleID) {
			return errors.New("default role must be one of the assigned roles")
		}
		return s.users.UpdateDefaultRole(ctx, id, input.DefaultRoleID)
	}
	if len(input.RoleIDs) == 0 {
		return s.users.UpdateDefaultRole(ctx, id, nil)
	}
	return nil
}

func (s *Service) RemoveRole(ctx context.Context, id uint64, roleID uint64) error {
	if s.users == nil {
		return errors.New("user repository not configured")
	}
	return s.users.RemoveRole(ctx, id, roleID)
}

type LinkEmployeeInput struct {
	EmployeeID uint64
	Reason     string
}

type LinkEmployeeResult struct {
	User          *model.User
	Action        string
	OldEmployeeID *uint64
	NewEmployeeID *uint64
	OldStatus     model.EmployeeLinkStatus
	NewStatus     model.EmployeeLinkStatus
	EmailMatch    bool
	OverrideUsed  bool
	Reason        string
}

func (s *Service) LinkEmployee(ctx context.Context, userID uint64, input LinkEmployeeInput) (*LinkEmployeeResult, error) {
	if s.users == nil {
		return nil, errors.New("user repository not configured")
	}
	if s.employees == nil {
		return nil, errors.New("employee repository not configured")
	}
	user, err := s.users.FindByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	employee, err := s.employees.FindByID(ctx, input.EmployeeID)
	if err != nil {
		return nil, err
	}

	oldEmployeeID := user.EmployeeID
	oldStatus := normalizeEmployeeLinkStatus(user.EmployeeLinkStatus)
	userEmail := strings.TrimSpace(user.Email)
	employeeEmail := strings.TrimSpace(employee.Email)
	emailMatch := userEmail != "" && employeeEmail != "" && strings.EqualFold(userEmail, employeeEmail)
	overrideUsed := !emailMatch
	isRelink := oldEmployeeID != nil && *oldEmployeeID != input.EmployeeID

	if (overrideUsed || isRelink) && strings.TrimSpace(input.Reason) == "" {
		return nil, errors.New("reason is required for repair or relink")
	}
	if userEmail == "" && strings.TrimSpace(input.Reason) == "" {
		return nil, errors.New("reason is required when user email is missing")
	}
	if employeeEmail == "" && strings.TrimSpace(input.Reason) == "" {
		return nil, errors.New("reason is required when employee email is missing")
	}

	user.EmployeeID = &input.EmployeeID
	user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	if employee.Name != "" {
		user.Name = employee.Name
	}
	if employee.AvatarURL != "" {
		user.AvatarURL = employee.AvatarURL
	}
	if err := s.users.CreateOrUpdate(ctx, user); err != nil {
		return nil, err
	}
	updated, err := s.users.FindByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	action := "user.link_employee"
	if isRelink {
		action = "user.relink_employee"
	} else if overrideUsed || oldStatus != model.EmployeeLinkStatusPending || oldEmployeeID != nil {
		action = "user.repair_employee_link"
	}

	return &LinkEmployeeResult{
		User:          updated,
		Action:        action,
		OldEmployeeID: oldEmployeeID,
		NewEmployeeID: updated.EmployeeID,
		OldStatus:     oldStatus,
		NewStatus:     normalizeEmployeeLinkStatus(updated.EmployeeLinkStatus),
		EmailMatch:    emailMatch,
		OverrideUsed:  overrideUsed,
		Reason:        strings.TrimSpace(input.Reason),
	}, nil
}

func normalizeEmployeeLinkStatus(status model.EmployeeLinkStatus) model.EmployeeLinkStatus {
	switch status {
	case model.EmployeeLinkStatusLinked, model.EmployeeLinkStatusFailed, model.EmployeeLinkStatusPending:
		return status
	default:
		return model.EmployeeLinkStatusPending
	}
}

type RoleListResult struct {
	Items []model.Role
	Total int64
}

func (s *Service) ListRoles(ctx context.Context, page, pageSize int) (*RoleListResult, error) {
	if s.roles == nil {
		return &RoleListResult{}, nil
	}
	items, total, err := s.roles.ListPaginated(ctx, page, pageSize)
	if err != nil {
		return nil, err
	}
	return &RoleListResult{Items: items, Total: total}, nil
}

func (s *Service) CreateRole(ctx context.Context, input RoleInput) (*model.Role, error) {
	if s.roles == nil {
		return nil, errors.New("role repository not configured")
	}
	role := &model.Role{Name: input.Name, Description: input.Description, IsDefault: input.IsDefault}
	if role.IsDefault {
		_ = s.roles.ClearDefault(ctx)
	}
	if err := s.roles.Create(ctx, role); err != nil {
		return nil, err
	}
	if len(input.PermissionIDs) > 0 {
		if err := s.roles.AssignPermissions(ctx, role.ID, input.PermissionIDs); err != nil {
			return nil, err
		}
		role, _ = s.roles.FindByID(ctx, role.ID)
	}
	return role, nil
}

func (s *Service) UpdateRole(ctx context.Context, id uint64, input RoleInput) (*model.Role, error) {
	if s.roles == nil {
		return nil, errors.New("role repository not configured")
	}
	role, err := s.roles.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if input.Name != "" {
		role.Name = input.Name
	}
	if input.Description != "" {
		role.Description = input.Description
	}
	if input.IsDefault {
		_ = s.roles.ClearDefault(ctx)
		role.IsDefault = true
	}
	if err := s.roles.Update(ctx, role); err != nil {
		return nil, err
	}
	if len(input.PermissionIDs) > 0 {
		if err := s.roles.AssignPermissions(ctx, role.ID, input.PermissionIDs); err != nil {
			return nil, err
		}
		role, _ = s.roles.FindByID(ctx, role.ID)
	}
	return role, nil
}

func (s *Service) DeleteRole(ctx context.Context, id uint64) error {
	if s.roles == nil {
		return errors.New("role repository not configured")
	}
	return s.roles.Delete(ctx, id)
}

func (s *Service) AssignPermissions(ctx context.Context, roleID uint64, permissionIDs []uint64) error {
	if s.roles == nil {
		return errors.New("role repository not configured")
	}
	return s.roles.AssignPermissions(ctx, roleID, permissionIDs)
}

func (s *Service) ListPermissions(ctx context.Context) ([]model.Permission, error) {
	if s.perms == nil {
		return []model.Permission{}, nil
	}
	return s.perms.List(ctx)
}

type RoleInput struct {
	Name          string
	Description   string
	IsDefault     bool
	PermissionIDs []uint64
}

func containsRole(roles []model.Role, roleID uint64) bool {
	for _, r := range roles {
		if r.ID == roleID {
			return true
		}
	}
	return false
}

func extractRoleIDs(roles []model.Role) []uint64 {
	ids := make([]uint64, 0, len(roles))
	for _, r := range roles {
		ids = append(ids, r.ID)
	}
	return ids
}

func containsID(ids []uint64, target uint64) bool {
	for _, id := range ids {
		if id == target {
			return true
		}
	}
	return false
}

// PermissionCatalog defines the canonical list of permissions with API/menu metadata.
func PermissionCatalog() []model.Permission {
	return []model.Permission{
		{
			Name:        "auth:manage_users",
			Description: "Manage user accounts and roles",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/users", "/api/v1/roles", "/api/v1/permissions"}, "menus": []string{"/dashboard/systems/users"}},
		},
		{
			Name:        "system:manage_settings",
			Description: "Manage global platform settings",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/system/config", "/api/v1/system/health", "/api/v1/system/version", "/api/v1/departments", "/api/v1/alerts"}, "menus": []string{"/dashboard/systems/settings"}},
		},
		{
			Name:        "caasm:view_assets",
			Description: "View CAASM assets",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/caasm"}, "menus": []string{"/dashboard/caasm/overview", "/dashboard/caasm/assets", "/dashboard/caasm/ip-assets", "/dashboard/caasm/api-assets", "/dashboard/caasm/domains"}},
		},
		{
			Name:        "caasm:manage_assets",
			Description: "Manage CAASM assets",
			Metadata: model.JSONB{
				"apis":  []string{"/api/v1/caasm/cloud-accounts", "/api/v1/caasm/sync-jobs"},
				"menus": []string{"/dashboard/caasm/sync-jobs", "/dashboard/caasm/config"},
			},
		},
		{
			Name:        "caasm:manage_api_assets",
			Description: "Manage CAASM API route asset configurations",
			Metadata: model.JSONB{
				"apis":  []string{"/api/v1/caasm/api-assets/config", "/api/v1/caasm/api-assets/config/preview"},
				"menus": []string{},
			},
		},
		{
			Name:        "caasm:export_ports",
			Description: "Export CAASM port inventory",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/caasm/ports/export"}, "menus": []string{"/dashboard/caasm/ports"}},
		},
		{
			Name:        "scan:view_batches",
			Description: "View scan batches and port exposure results",
			Metadata: model.JSONB{
				"apis": []string{
					"/api/v1/scans/port-exposures",
					"/api/v1/scans/stats",
					"/api/v1/scans/campaigns",
					"/api/v1/scans/campaigns/:id",
					"/api/v1/scans/campaigns/:id/batches",
					"/api/v1/scans/batches",
					"/api/v1/scans/batches/:id",
					"/api/v1/scans/batches/:id/targets",
					"/api/v1/scans/batches/:id/observations",
					"/api/v1/scans/batches/:id/callback-receipts",
				},
				"menus": []string{"/dashboard/scans/port-exposure"},
			},
		},
		{
			Name:        "scan:create_batches",
			Description: "Create scan batches from platform assets",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/scans/campaigns", "/api/v1/scans/batches"}, "menus": []string{"/dashboard/scans/port-exposure"}},
		},
		{
			Name:        "scan:manage_batches",
			Description: "Manage scan batch sync and operational recovery",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/scans/campaigns/:id/sync", "/api/v1/scans/batches/:id/sync"}, "menus": []string{"/dashboard/scans/port-exposure"}},
		},
		{
			Name:        "scan:view_evidence",
			Description: "View scanner evidence details for scan results",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/scans/batches/:id/observations", "/api/v1/scans/batches/:id/callback-receipts"}, "menus": []string{"/dashboard/scans/port-exposure"}},
		},
		{
			Name:        "vuln:view_findings",
			Description: "View vulnerabilities",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities"}, "menus": []string{"/dashboard/vulnerabilities/list"}},
		},
		{
			Name:        "vuln:manage_findings",
			Description: "Manage vulnerabilities",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities"}, "menus": []string{"/dashboard/vulnerabilities/config"}},
		},
		{
			Name:        "vuln:submit_findings",
			Description: "Submit vulnerability findings",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities"}, "menus": []string{"/dashboard/vulnerabilities/submit"}},
		},
		{
			Name:        "vuln:view_sensitive_details",
			Description: "View sensitive vulnerability verification details",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "vuln:edit_sensitive_details",
			Description: "Edit sensitive vulnerability verification details",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id/meta"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "vuln:edit_details",
			Description: "Edit vulnerability detail fields",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id"}, "menus": []string{"/dashboard/vulnerabilities/:public_id", "/dashboard/vulnerabilities/:public_id/edit"}},
		},
		{
			Name:        "vuln:authorize_viewers",
			Description: "Manage authorized viewers and public visibility for restricted vulnerabilities",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id/viewers", "/api/v1/vulnerabilities/:public_id/visibility"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "vuln:assign_owner",
			Description: "Assign vulnerability owners",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id/assign"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "vuln:request_retest",
			Description: "Request security retest after remediation",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id/transition"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "vuln:close_findings",
			Description: "Close or reopen vulnerabilities after security review",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/vulnerabilities/:public_id/transition"}, "menus": []string{"/dashboard/vulnerabilities/:public_id"}},
		},
		{
			Name:        "audit:view_logs",
			Description: "View audit logs",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/audit-logs"}, "menus": []string{"/dashboard/logs/audit"}},
		},
		{
			Name:        "agent:manage_own_keys",
			Description: "Manage own agent API keys",
			Metadata:    model.JSONB{"apis": []string{"/api/v1/agent/api-keys"}, "menus": []string{}},
		},
	}
}

// SeedPermissions upserts the catalog and ensures baseline roles have required permissions if they exist.
func SeedPermissions(ctx context.Context, permRepo repository.PermissionRepository, roleRepo repository.RoleRepository) error {
	if permRepo == nil {
		return errors.New("permission repository not configured")
	}
	if err := permRepo.Upsert(ctx, PermissionCatalog()); err != nil {
		return err
	}
	if roleRepo == nil {
		return nil
	}
	perms, err := permRepo.List(ctx)
	if err != nil {
		return err
	}
	permIDsByName := make(map[string]uint64, len(perms))
	var allIDs []uint64
	for _, p := range perms {
		permIDsByName[p.Name] = p.ID
		allIDs = append(allIDs, p.ID)
	}
	if len(allIDs) == 0 {
		return nil
	}
	if err := assignRolePermissionsByName(ctx, roleRepo, "admin", allIDs, nil, nil); err != nil {
		return err
	}
	if err := assignRolePermissionsByName(ctx, roleRepo, "security_operator", nil, []string{
		"caasm:view_assets",
		"caasm:manage_assets",
		"caasm:manage_api_assets",
		"caasm:export_ports",
		"scan:view_batches",
		"scan:create_batches",
		"scan:manage_batches",
		"scan:view_evidence",
		"vuln:view_findings",
		"vuln:manage_findings",
		"vuln:submit_findings",
		"vuln:view_sensitive_details",
		"vuln:edit_sensitive_details",
		"vuln:authorize_viewers",
		"vuln:assign_owner",
		"vuln:request_retest",
		"vuln:close_findings",
		"audit:view_logs",
	}, permIDsByName); err != nil {
		return err
	}
	// agent_user 是内置默认角色：新用户首登开通时授予（见 auth.ensureDefaultRole 调用方），
	// 存量用户不追溯。此处幂等保证角色存在并持有 agent:manage_own_keys。
	if err := ensureRoleExists(ctx, roleRepo, "agent_user", "Default role granting agent self-service API key management"); err != nil {
		return err
	}
	return assignRolePermissionsByName(ctx, roleRepo, "agent_user", nil, []string{
		"agent:manage_own_keys",
	}, permIDsByName)
}

// ensureRoleExists creates the named role when missing; repeated calls are no-ops.
func ensureRoleExists(ctx context.Context, roleRepo repository.RoleRepository, name, description string) error {
	if _, err := roleRepo.FindByName(ctx, name); err == nil {
		return nil
	}
	role := &model.Role{Name: name, Description: description}
	if err := roleRepo.Create(ctx, role); err != nil {
		// 并发启动或角色已存在时回退再查一次，保持幂等。
		if _, findErr := roleRepo.FindByName(ctx, name); findErr != nil {
			return err
		}
	}
	return nil
}

func assignRolePermissionsByName(ctx context.Context, roleRepo repository.RoleRepository, roleName string, requiredIDs []uint64, requiredNames []string, permIDsByName map[string]uint64) error {
	role, err := roleRepo.FindByName(ctx, roleName)
	if err != nil {
		return nil // Don't fail startup if the role is not present yet.
	}
	seen := make(map[uint64]struct{}, len(role.Permissions)+len(requiredIDs)+len(requiredNames))
	merged := make([]uint64, 0, len(role.Permissions)+len(requiredIDs)+len(requiredNames))
	for _, permission := range role.Permissions {
		if permission.ID == 0 {
			continue
		}
		if _, ok := seen[permission.ID]; ok {
			continue
		}
		seen[permission.ID] = struct{}{}
		merged = append(merged, permission.ID)
	}
	for _, id := range requiredIDs {
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		merged = append(merged, id)
	}
	for _, name := range requiredNames {
		id := permIDsByName[name]
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		merged = append(merged, id)
	}
	if len(merged) == 0 {
		return nil
	}
	return roleRepo.AssignPermissions(ctx, role.ID, merged)
}
