using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.TenantApi.Contracts.Store;

namespace TakeoutSaaS.TenantApi.Controllers;

/// <summary>
/// 门店员工与排班模块。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/tenant/v{version:apiVersion}/store")]
public sealed class StoreStaffController(
    TakeoutAppDbContext dbContext,
    StoreContextService storeContextService) : BaseApiController
{
    /// <summary>
    /// 获取员工分页列表。
    /// </summary>
    [HttpGet("staff")]
    [ProducesResponseType(typeof(ApiResponse<PaginatedResultDto<StoreStaffItemDto>>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<PaginatedResultDto<StoreStaffItemDto>>> List(
        [FromQuery] string storeId,
        [FromQuery] string? keyword,
        [FromQuery] string? roleType,
        [FromQuery] string? status,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 10,
        CancellationToken cancellationToken = default)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var normalizedKeyword = keyword?.Trim();
        StaffRoleType? normalizedRoleType = string.IsNullOrWhiteSpace(roleType) ? null : StoreApiHelpers.ToStaffRoleType(roleType);
        StaffStatus? normalizedStatus = string.IsNullOrWhiteSpace(status) ? null : StoreApiHelpers.ToStaffStatus(status);
        var normalizedPage = Math.Max(1, page);
        var normalizedPageSize = Math.Clamp(pageSize, 1, 200);

        var query = dbContext.MerchantStaff
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId);

        if (!string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            var lowered = normalizedKeyword.ToLowerInvariant();
            query = query.Where(x =>
                x.Name.ToLower().Contains(lowered) ||
                x.Phone.Contains(normalizedKeyword) ||
                (x.Email != null && x.Email.ToLower().Contains(lowered)));
        }

        if (normalizedRoleType.HasValue)
        {
            query = query.Where(x => x.RoleType == normalizedRoleType.Value);
        }

        if (normalizedStatus.HasValue)
        {
            query = query.Where(x => x.Status == normalizedStatus.Value);
        }

        var total = await query.CountAsync(cancellationToken);
        var staffs = await query
            .OrderBy(x => x.CreatedAt)
            .ThenBy(x => x.Name)
            .Skip((normalizedPage - 1) * normalizedPageSize)
            .Take(normalizedPageSize)
            .ToListAsync(cancellationToken);

        var result = new PaginatedResultDto<StoreStaffItemDto>
        {
            Items = staffs.Select(MapStaff).ToList(),
            Total = total,
            Page = normalizedPage,
            PageSize = normalizedPageSize
        };

        return ApiResponse<PaginatedResultDto<StoreStaffItemDto>>.Ok(result);
    }

    /// <summary>
    /// 保存员工。
    /// </summary>
    [HttpPost("staff/save")]
    [ProducesResponseType(typeof(ApiResponse<StoreStaffItemDto>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<StoreStaffItemDto>> Save([FromBody] SaveStoreStaffRequest request, CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        var store = await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var staffId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id);
        var roleType = StoreApiHelpers.ToStaffRoleType(request.RoleType);
        var staffStatus = StoreApiHelpers.ToStaffStatus(request.Status);
        var permissions = NormalizePermissions(request.Permissions, roleType);

        MerchantStaff? entity = null;
        if (staffId.HasValue)
        {
            entity = await dbContext.MerchantStaff.FirstOrDefaultAsync(
                x => x.Id == staffId.Value && x.TenantId == tenantId && x.StoreId == parsedStoreId,
                cancellationToken);
        }

        if (entity is null)
        {
            entity = new MerchantStaff
            {
                MerchantId = store.MerchantId,
                StoreId = parsedStoreId
            };
            await dbContext.MerchantStaff.AddAsync(entity, cancellationToken);
        }

        entity.Name = request.Name.Trim();
        entity.Phone = request.Phone.Trim();
        entity.Email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
        entity.RoleType = roleType;
        entity.Status = staffStatus;
        entity.PermissionsJson = JsonSerializer.Serialize(permissions, StoreApiHelpers.JsonOptions);

        await dbContext.SaveChangesAsync(cancellationToken);

        return ApiResponse<StoreStaffItemDto>.Ok(MapStaff(entity));
    }

    /// <summary>
    /// 删除员工。
    /// </summary>
    [HttpPost("staff/delete")]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<object>> Delete([FromBody] DeleteStoreStaffRequest request, CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
        var staffId = StoreApiHelpers.ParseRequiredSnowflake(request.StaffId, "staffId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var staff = await dbContext.MerchantStaff.FirstOrDefaultAsync(
            x => x.Id == staffId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
            cancellationToken);
        if (staff is null)
        {
            return ApiResponse<object>.Ok(null);
        }

        dbContext.MerchantStaff.Remove(staff);

        var schedules = await dbContext.StoreStaffWeeklySchedules
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.StaffId == staffId)
            .ToListAsync(cancellationToken);
        dbContext.StoreStaffWeeklySchedules.RemoveRange(schedules);

        await dbContext.SaveChangesAsync(cancellationToken);
        return ApiResponse<object>.Ok(null);
    }

    /// <summary>
    /// 获取门店排班配置。
    /// </summary>
    [HttpGet("staff/schedule")]
    [ProducesResponseType(typeof(ApiResponse<StoreStaffScheduleDto>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<StoreStaffScheduleDto>> GetSchedule(
        [FromQuery] string storeId,
        [FromQuery] string? weekStartDate,
        CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
        var staffs = await dbContext.MerchantStaff
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
            .OrderBy(x => x.CreatedAt)
            .ThenBy(x => x.Name)
            .ToListAsync(cancellationToken);

        var scheduleRows = await dbContext.StoreStaffWeeklySchedules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
            .ToListAsync(cancellationToken);
        var scheduleMap = scheduleRows
            .GroupBy(x => x.StaffId)
            .ToDictionary(x => x.Key, x => x.ToList());

        var schedules = staffs.Select(staff =>
        {
            var shifts = scheduleMap.TryGetValue(staff.Id, out var rows)
                ? template is null
                    ? []
                    : NormalizeRowsToShifts(rows, template)
                : [];

            if (staff.Status == StaffStatus.Resigned)
            {
                shifts = CreateOffWeekShifts();
            }

            return new StaffScheduleDto
            {
                StaffId = staff.Id.ToString(),
                Shifts = shifts
            };
        }).ToList();

        return ApiResponse<StoreStaffScheduleDto>.Ok(new StoreStaffScheduleDto
        {
            StoreId = parsedStoreId.ToString(),
            WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(weekStartDate),
            Templates = template,
            IsTemplateConfigured = template is not null,
            IsScheduleConfigured = scheduleRows.Count > 0,
            Schedules = schedules
        });
    }

    /// <summary>
    /// 保存班次模板。
    /// </summary>
    [HttpPost("staff/template/save")]
    [ProducesResponseType(typeof(ApiResponse<StoreShiftTemplatesDto>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<StoreShiftTemplatesDto>> SaveTemplate(
        [FromBody] SaveStoreStaffTemplatesRequest request,
        CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var normalizedTemplate = NormalizeTemplate(request.Templates);
        var templateEntity = await dbContext.StoreStaffTemplates.FirstOrDefaultAsync(
            x => x.TenantId == tenantId && x.StoreId == parsedStoreId,
            cancellationToken);
        if (templateEntity is null)
        {
            templateEntity = new StoreStaffTemplate
            {
                StoreId = parsedStoreId
            };
            await dbContext.StoreStaffTemplates.AddAsync(templateEntity, cancellationToken);
        }

        templateEntity.MorningStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Morning.StartTime, "templates.morning.startTime");
        templateEntity.MorningEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Morning.EndTime, "templates.morning.endTime");
        templateEntity.EveningStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Evening.StartTime, "templates.evening.startTime");
        templateEntity.EveningEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Evening.EndTime, "templates.evening.endTime");
        templateEntity.FullStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Full.StartTime, "templates.full.startTime");
        templateEntity.FullEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Full.EndTime, "templates.full.endTime");

        var scheduleRows = await dbContext.StoreStaffWeeklySchedules
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
            .ToListAsync(cancellationToken);
        foreach (var row in scheduleRows)
        {
            var (start, end) = ResolveShiftTimeRange(row.ShiftType, normalizedTemplate);
            row.StartTime = start;
            row.EndTime = end;
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return ApiResponse<StoreShiftTemplatesDto>.Ok(normalizedTemplate);
    }

    /// <summary>
    /// 保存单员工排班。
    /// </summary>
    [HttpPost("staff/schedule/personal/save")]
    [ProducesResponseType(typeof(ApiResponse<StaffScheduleDto>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<StaffScheduleDto>> SavePersonalSchedule(
        [FromBody] SaveStoreStaffPersonalScheduleRequest request,
        CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
        var parsedStaffId = StoreApiHelpers.ParseRequiredSnowflake(request.StaffId, "staffId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var staff = await dbContext.MerchantStaff.FirstOrDefaultAsync(
            x => x.Id == parsedStaffId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
            cancellationToken);
        if (staff is null)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "员工不存在");
        }

        var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
        if (template is null)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "请先配置班次模板");
        }
        var existingRows = await dbContext.StoreStaffWeeklySchedules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.StaffId == parsedStaffId)
            .ToListAsync(cancellationToken);

        var fallback = NormalizeRowsToShifts(existingRows, template);
        var shifts = staff.Status == StaffStatus.Resigned
            ? CreateOffWeekShifts()
            : NormalizeShifts(request.Shifts, fallback, template);

        var nextRows = ToWeeklyEntities(parsedStoreId, parsedStaffId, shifts, template).ToList();
        await ReplaceWeeklySchedulesAsync(
            dbContext.StoreStaffWeeklySchedules
                .Where(x =>
                    x.TenantId == tenantId &&
                    x.StoreId == parsedStoreId &&
                    x.StaffId == parsedStaffId),
            nextRows,
            cancellationToken);

        return ApiResponse<StaffScheduleDto>.Ok(new StaffScheduleDto
        {
            StaffId = parsedStaffId.ToString(),
            Shifts = shifts
        });
    }

    /// <summary>
    /// 保存门店周排班。
    /// </summary>
    [HttpPost("staff/schedule/weekly/save")]
    [ProducesResponseType(typeof(ApiResponse<StoreStaffScheduleDto>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<StoreStaffScheduleDto>> SaveWeeklySchedule(
        [FromBody] SaveStoreStaffWeeklyScheduleRequest request,
        CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);

        var staffs = await dbContext.MerchantStaff
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
            .OrderBy(x => x.CreatedAt)
            .ThenBy(x => x.Name)
            .ToListAsync(cancellationToken);
        var staffMap = staffs.ToDictionary(x => x.Id);

        var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
        if (template is null)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "请先配置班次模板");
        }
        var existingRows = await dbContext.StoreStaffWeeklySchedules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
            .ToListAsync(cancellationToken);
        var existingMap = existingRows
            .GroupBy(x => x.StaffId)
            .ToDictionary(
                x => x.Key,
                x => NormalizeRowsToShifts(x.ToList(), template));

        var incomingMap = new Dictionary<long, List<StaffDayShiftDto>>();
        foreach (var schedule in request.Schedules ?? [])
        {
            var staffId = StoreApiHelpers.ParseSnowflakeOrNull(schedule.StaffId);
            if (!staffId.HasValue || !staffMap.TryGetValue(staffId.Value, out var staff))
            {
                continue;
            }

            if (staff.Status == StaffStatus.Resigned)
            {
                continue;
            }

            var fallback = existingMap.GetValueOrDefault(staffId.Value) ?? [];
            incomingMap[staffId.Value] = NormalizeShifts(schedule.Shifts, fallback, template);
        }

        var finalSchedules = new List<StaffScheduleDto>();
        foreach (var staff in staffs)
        {
            List<StaffDayShiftDto> shifts;
            if (staff.Status == StaffStatus.Resigned)
            {
                shifts = CreateOffWeekShifts();
            }
            else if (incomingMap.TryGetValue(staff.Id, out var incomingShifts))
            {
                shifts = incomingShifts;
            }
            else
            {
                shifts = existingMap.GetValueOrDefault(staff.Id) ?? [];
            }

            finalSchedules.Add(new StaffScheduleDto
            {
                StaffId = staff.Id.ToString(),
                Shifts = shifts
            });
        }

        var entities = finalSchedules
            .SelectMany(x => ToWeeklyEntities(parsedStoreId, long.Parse(x.StaffId), x.Shifts, template))
            .ToList();
        await ReplaceWeeklySchedulesAsync(
            dbContext.StoreStaffWeeklySchedules
                .Where(x =>
                    x.TenantId == tenantId &&
                    x.StoreId == parsedStoreId),
            entities,
            cancellationToken);

        return ApiResponse<StoreStaffScheduleDto>.Ok(new StoreStaffScheduleDto
        {
            StoreId = parsedStoreId.ToString(),
            WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(null),
            Templates = template,
            IsTemplateConfigured = true,
            IsScheduleConfigured = entities.Count > 0,
            Schedules = finalSchedules
        });
    }

    /// <summary>
    /// 复制班次模板与排班。
    /// </summary>
    [HttpPost("staff/copy")]
    [ProducesResponseType(typeof(ApiResponse<CopyStoreStaffScheduleResult>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<CopyStoreStaffScheduleResult>> Copy([FromBody] CopyStoreStaffScheduleRequest request, CancellationToken cancellationToken)
    {
        var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
        var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
        var copyScope = string.IsNullOrWhiteSpace(request.CopyScope) ? "template_and_schedule" : request.CopyScope;

        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);

        if (!string.Equals(copyScope, "template_and_schedule", StringComparison.OrdinalIgnoreCase))
        {
            return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
            {
                CopiedCount = 0,
                CopyScope = "template_and_schedule"
            });
        }

        var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
            dbContext,
            tenantId,
            merchantId,
            targetStoreIds,
            cancellationToken);
        accessibleTargetIds.Remove(sourceStoreId);

        if (accessibleTargetIds.Count == 0)
        {
            return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
            {
                CopiedCount = 0,
                CopyScope = "template_and_schedule"
            });
        }

        var sourceTemplate = await GetTemplateDtoAsync(tenantId, sourceStoreId, cancellationToken);
        if (sourceTemplate is null)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置班次模板，无法复制");
        }
        var sourceRows = await dbContext.StoreStaffWeeklySchedules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
            .ToListAsync(cancellationToken);
        var sourceStaffs = await dbContext.MerchantStaff
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
            .ToListAsync(cancellationToken);
        var sourceStaffMap = sourceStaffs.ToDictionary(x => x.Id);
        var sourceScheduleMap = sourceRows
            .GroupBy(x => x.StaffId)
            .ToDictionary(
                x => x.Key,
                x => NormalizeRowsToShifts(x.ToList(), sourceTemplate));
        var sourceScheduleSequence = sourceStaffs
            .OrderBy(x => x.CreatedAt)
            .ThenBy(x => x.Name)
            .ThenBy(x => x.Id)
            .Select(staff => staff.Status == StaffStatus.Resigned
                ? CreateOffWeekShifts()
                : sourceScheduleMap.GetValueOrDefault(staff.Id) ?? [])
            .ToList();

        var targetTemplates = await dbContext.StoreStaffTemplates
            .Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
            .ToListAsync(cancellationToken);
        var targetTemplateMap = targetTemplates.ToDictionary(x => x.StoreId);

        foreach (var targetStoreId in accessibleTargetIds)
        {
            if (!targetTemplateMap.TryGetValue(targetStoreId, out var targetTemplateEntity))
            {
                targetTemplateEntity = new StoreStaffTemplate
                {
                    StoreId = targetStoreId
                };
                await dbContext.StoreStaffTemplates.AddAsync(targetTemplateEntity, cancellationToken);
            }

            targetTemplateEntity.MorningStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Morning.StartTime, "templates.morning.startTime");
            targetTemplateEntity.MorningEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Morning.EndTime, "templates.morning.endTime");
            targetTemplateEntity.EveningStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Evening.StartTime, "templates.evening.startTime");
            targetTemplateEntity.EveningEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Evening.EndTime, "templates.evening.endTime");
            targetTemplateEntity.FullStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Full.StartTime, "templates.full.startTime");
            targetTemplateEntity.FullEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Full.EndTime, "templates.full.endTime");
        }
        await dbContext.SaveChangesAsync(cancellationToken);

        var targetStaffs = await dbContext.MerchantStaff
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId.HasValue && accessibleTargetIds.Contains(x.StoreId.Value))
            .ToListAsync(cancellationToken);

        var entities = new List<StoreStaffWeeklySchedule>();
        foreach (var targetStoreId in accessibleTargetIds)
        {
            var targetStoreStaffs = targetStaffs
                .Where(x => x.StoreId == targetStoreId)
                .OrderBy(x => x.CreatedAt)
                .ThenBy(x => x.Name)
                .ThenBy(x => x.Id)
                .ToList();

            for (var index = 0; index < targetStoreStaffs.Count; index++)
            {
                var staff = targetStoreStaffs[index];
                var shifts = staff.Status == StaffStatus.Resigned
                    ? CreateOffWeekShifts()
                    : sourceScheduleSequence.Count > 0
                        ? sourceScheduleSequence[index % sourceScheduleSequence.Count]
                        : [];

                entities.AddRange(ToWeeklyEntities(targetStoreId, staff.Id, shifts, sourceTemplate));
            }
        }

        await ReplaceWeeklySchedulesAsync(
            dbContext.StoreStaffWeeklySchedules
                .Where(x =>
                    x.TenantId == tenantId &&
                    accessibleTargetIds.Contains(x.StoreId)),
            entities,
            cancellationToken);

        return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
        {
            CopiedCount = accessibleTargetIds.Count,
            CopyScope = "template_and_schedule"
        });
    }
    private static StoreStaffItemDto MapStaff(MerchantStaff source)
    {
        var permissions = ParsePermissions(source.PermissionsJson, source.RoleType);
        var hiredAt = source.CreatedAt == default ? DateTime.UtcNow : source.CreatedAt;

        return new StoreStaffItemDto
        {
            Id = source.Id.ToString(),
            Name = source.Name,
            Phone = source.Phone,
            Email = source.Email ?? string.Empty,
            RoleType = StoreApiHelpers.ToStaffRoleTypeText(source.RoleType),
            Status = StoreApiHelpers.ToStaffStatusText(source.Status),
            Permissions = permissions,
            AvatarColor = StoreApiHelpers.ResolveAvatarColor($"{source.Name}-{source.Id}"),
            HiredAt = StoreApiHelpers.ToDateOnly(hiredAt)
        };
    }

    private static List<string> ParsePermissions(string? rawJson, StaffRoleType roleType)
    {
        if (!string.IsNullOrWhiteSpace(rawJson))
        {
            try
            {
                var parsed = JsonSerializer.Deserialize<List<string>>(rawJson, StoreApiHelpers.JsonOptions);
                if (parsed is not null)
                {
                    return NormalizePermissions(parsed, roleType);
                }
            }
            catch
            {
                // 忽略权限反序列化异常并回落默认值
            }
        }

        return roleType is StaffRoleType.Admin or StaffRoleType.Operator ? ["全部权限"] : [];
    }

    private static List<string> NormalizePermissions(IEnumerable<string>? source, StaffRoleType roleType)
    {
        var normalized = (source ?? [])
            .Select(x => x?.Trim())
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Select(x => x!)
            .Distinct()
            .ToList();

        if (roleType is StaffRoleType.Admin or StaffRoleType.Operator && normalized.Count == 0)
        {
            normalized.Add("全部权限");
        }

        return normalized;
    }

    private async Task<StoreShiftTemplatesDto?> GetTemplateDtoAsync(long tenantId, long storeId, CancellationToken cancellationToken)
    {
        var entity = await dbContext.StoreStaffTemplates
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == storeId, cancellationToken);

        if (entity is null)
        {
            return null;
        }

        return new StoreShiftTemplatesDto
        {
            Morning = new ShiftTemplateItemDto
            {
                StartTime = StoreApiHelpers.ToHHmm(entity.MorningStartTime),
                EndTime = StoreApiHelpers.ToHHmm(entity.MorningEndTime)
            },
            Evening = new ShiftTemplateItemDto
            {
                StartTime = StoreApiHelpers.ToHHmm(entity.EveningStartTime),
                EndTime = StoreApiHelpers.ToHHmm(entity.EveningEndTime)
            },
            Full = new ShiftTemplateItemDto
            {
                StartTime = StoreApiHelpers.ToHHmm(entity.FullStartTime),
                EndTime = StoreApiHelpers.ToHHmm(entity.FullEndTime)
            }
        };
    }

    private static StoreShiftTemplatesDto NormalizeTemplate(StoreShiftTemplatesDto source)
    {
        source ??= new StoreShiftTemplatesDto();
        return new StoreShiftTemplatesDto
        {
            Morning = new ShiftTemplateItemDto
            {
                StartTime = NormalizeTime(source.Morning.StartTime, string.Empty),
                EndTime = NormalizeTime(source.Morning.EndTime, string.Empty)
            },
            Evening = new ShiftTemplateItemDto
            {
                StartTime = NormalizeTime(source.Evening.StartTime, string.Empty),
                EndTime = NormalizeTime(source.Evening.EndTime, string.Empty)
            },
            Full = new ShiftTemplateItemDto
            {
                StartTime = NormalizeTime(source.Full.StartTime, string.Empty),
                EndTime = NormalizeTime(source.Full.EndTime, string.Empty)
            }
        };
    }

    private static List<StaffDayShiftDto> NormalizeRowsToShifts(
        List<StoreStaffWeeklySchedule> rows,
        StoreShiftTemplatesDto template)
    {
        var results = new List<StaffDayShiftDto>();
        foreach (var row in rows
                     .Where(x => x.DayOfWeek is >= 0 and <= 6)
                     .OrderBy(x => x.DayOfWeek))
        {
            var shiftType = StoreApiHelpers.ToShiftTypeText(row.ShiftType);
            if (row.ShiftType == StoreStaffShiftType.Off)
            {
                results.Add(new StaffDayShiftDto
                {
                    DayOfWeek = row.DayOfWeek,
                    ShiftType = shiftType,
                    StartTime = string.Empty,
                    EndTime = string.Empty
                });
                continue;
            }

            var (defaultStart, defaultEnd) = ResolveShiftTimeRange(row.ShiftType, template);
            results.Add(new StaffDayShiftDto
            {
                DayOfWeek = row.DayOfWeek,
                ShiftType = shiftType,
                StartTime = StoreApiHelpers.ToHHmm(row.StartTime ?? defaultStart ?? TimeSpan.Zero),
                EndTime = StoreApiHelpers.ToHHmm(row.EndTime ?? defaultEnd ?? TimeSpan.Zero)
            });
        }

        return results;
    }

    private static List<StaffDayShiftDto> NormalizeShifts(
        IEnumerable<StaffDayShiftDto>? source,
        List<StaffDayShiftDto> fallback,
        StoreShiftTemplatesDto template)
    {
        var inputMap = (source ?? [])
            .Where(x => x.DayOfWeek is >= 0 and <= 6)
            .GroupBy(x => x.DayOfWeek)
            .ToDictionary(x => x.Key, x => x.Last());

        var normalized = new List<StaffDayShiftDto>();
        for (var day = 0; day < 7; day++)
        {
            var fallbackShift = fallback.FirstOrDefault(x => x.DayOfWeek == day);

            if (!inputMap.TryGetValue(day, out var input))
            {
                if (fallbackShift is null) continue;
                normalized.Add(new StaffDayShiftDto
                {
                    DayOfWeek = day,
                    ShiftType = fallbackShift.ShiftType,
                    StartTime = fallbackShift.StartTime,
                    EndTime = fallbackShift.EndTime
                });
                continue;
            }

            var shiftType = StoreApiHelpers.ToShiftType(input.ShiftType);
            if (shiftType == StoreStaffShiftType.Off)
            {
                normalized.Add(new StaffDayShiftDto
                {
                    DayOfWeek = day,
                    ShiftType = "off",
                    StartTime = string.Empty,
                    EndTime = string.Empty
                });
                continue;
            }

            var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, template);
            normalized.Add(new StaffDayShiftDto
            {
                DayOfWeek = day,
                ShiftType = StoreApiHelpers.ToShiftTypeText(shiftType),
                StartTime = NormalizeTime(
                    input.StartTime,
                    defaultStart.HasValue
                        ? StoreApiHelpers.ToHHmm(defaultStart.Value)
                        : fallbackShift?.StartTime ?? string.Empty),
                EndTime = NormalizeTime(
                    input.EndTime,
                    defaultEnd.HasValue
                        ? StoreApiHelpers.ToHHmm(defaultEnd.Value)
                        : fallbackShift?.EndTime ?? string.Empty)
            });
        }

        return normalized;
    }

    private static IEnumerable<StoreStaffWeeklySchedule> ToWeeklyEntities(
        long storeId,
        long staffId,
        IEnumerable<StaffDayShiftDto> shifts,
        StoreShiftTemplatesDto template)
    {
        foreach (var shift in shifts
                     .Where(x => x.DayOfWeek is >= 0 and <= 6)
                     .GroupBy(x => x.DayOfWeek)
                     .Select(x => x.Last())
                     .OrderBy(x => x.DayOfWeek))
        {
            var shiftType = StoreApiHelpers.ToShiftType(shift.ShiftType);
            var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, template);
            var startTime = shiftType == StoreStaffShiftType.Off
                ? null
                : (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
                    NormalizeTime(shift.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : string.Empty),
                    "shift.startTime");
            var endTime = shiftType == StoreStaffShiftType.Off
                ? null
                : (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
                    NormalizeTime(shift.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : string.Empty),
                    "shift.endTime");

            yield return new StoreStaffWeeklySchedule
            {
                StoreId = storeId,
                StaffId = staffId,
                DayOfWeek = shift.DayOfWeek,
                ShiftType = shiftType,
                StartTime = startTime,
                EndTime = endTime
            };
        }
    }

    private async Task ReplaceWeeklySchedulesAsync(
        IQueryable<StoreStaffWeeklySchedule> deleteQuery,
        IReadOnlyCollection<StoreStaffWeeklySchedule> nextRows,
        CancellationToken cancellationToken)
    {
        var nextRowPayloads = nextRows
            .Select(x => new
            {
                x.StoreId,
                x.StaffId,
                x.DayOfWeek,
                x.ShiftType,
                x.StartTime,
                x.EndTime
            })
            .ToList();

        var executionStrategy = dbContext.Database.CreateExecutionStrategy();
        await executionStrategy.ExecuteAsync(async () =>
        {
            dbContext.ChangeTracker.Clear();

            await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
            await deleteQuery.IgnoreQueryFilters().ExecuteDeleteAsync(cancellationToken);

            if (nextRowPayloads.Count > 0)
            {
                var insertRows = nextRowPayloads.Select(x => new StoreStaffWeeklySchedule
                {
                    StoreId = x.StoreId,
                    StaffId = x.StaffId,
                    DayOfWeek = x.DayOfWeek,
                    ShiftType = x.ShiftType,
                    StartTime = x.StartTime,
                    EndTime = x.EndTime
                }).ToList();

                await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(insertRows, cancellationToken);
                await dbContext.SaveChangesAsync(cancellationToken);
            }

            await transaction.CommitAsync(cancellationToken);
        });
    }

    private static List<StaffDayShiftDto> CreateOffWeekShifts()
    {
        return Enumerable.Range(0, 7).Select(day => new StaffDayShiftDto
        {
            DayOfWeek = day,
            ShiftType = "off",
            StartTime = string.Empty,
            EndTime = string.Empty
        }).ToList();
    }

    private static (TimeSpan? Start, TimeSpan? End) ResolveShiftTimeRange(StoreStaffShiftType shiftType, StoreShiftTemplatesDto template)
    {
        return shiftType switch
        {
            StoreStaffShiftType.Morning => (
                StoreApiHelpers.ParseRequiredTime(template.Morning.StartTime, "templates.morning.startTime"),
                StoreApiHelpers.ParseRequiredTime(template.Morning.EndTime, "templates.morning.endTime")),
            StoreStaffShiftType.Evening => (
                StoreApiHelpers.ParseRequiredTime(template.Evening.StartTime, "templates.evening.startTime"),
                StoreApiHelpers.ParseRequiredTime(template.Evening.EndTime, "templates.evening.endTime")),
            StoreStaffShiftType.Full => (
                StoreApiHelpers.ParseRequiredTime(template.Full.StartTime, "templates.full.startTime"),
                StoreApiHelpers.ParseRequiredTime(template.Full.EndTime, "templates.full.endTime")),
            _ => (null, null)
        };
    }

    private static string NormalizeTime(string? value, string fallback)
    {
        return TimeSpan.TryParseExact(value, "hh\\:mm", null, out var parsed)
            ? StoreApiHelpers.ToHHmm(parsed)
            : fallback;
    }
}
