import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { buildClaimUrl } from '../../claim/claim-service.js'
import { listCloudtentaclesSku } from '../../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../../platforms/cloudtentacles/knapsack-service.js'
import {
  backCloudtentaclesVirtualNumber,
  getCloudtentaclesBindUrl,
} from '../../platforms/cloudtentacles/virtual-number-service.js'
import { notifyKuaishouCloudBindUrlRefreshFailed } from '../../notification/domain-notifications.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
import {
  KUAISHOU_CLOUD_FIXED_VN_KEY,
  isKuaishouCloudBindUrlFresh,
  isKuaishouCloudBindingMutationFrozen,
  isKuaishouCloudTask,
  maskPhone,
  normalizeKuaishouCloudFlow,
  normalizeStringArray,
  resolveKuaishouCloudBindUrlExpiresAt,
  type JsonObject,
} from './domain.js'
import {
  prepareKuaishouCloudBindResourceWithFallback,
  resolveKuaishouCloudBindingResources,
  resolveKuaishouCloudVnKeyCandidates,
} from './binding-resources.js'
import {
  buildCloudtentaclesSourceSelectionEventDetail,
  selectCloudtentaclesSourceForFulfillment,
} from './account-selector.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import { wrapCloudtentaclesOperationError } from './cloudtentacles-errors.js'
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
import { ensureTaskClaimLink } from './ensure-claim-link.js'
import {
  buildPendingRoleWithDefaultSnapshot,
  captureKuaishouCloudDefaultRoleSnapshot,
} from './role-state.js'
import type { TaskRow } from '../../../types/repository/rows.js'

/**
 * 准备 / 重建绑定资源（不退款、不重建订单/claim）。
 * - 默认：已 ready 且绑链未过期则直接复用
 * - force=true：旧号 best-effort 退还（已被系统回收则忽略），再取新号+新绑链
 */
export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) {
  if (!isKuaishouCloudTask(task)) {
    throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
      statusCode: 409,
      errorCode: 'kuaishou_cloud_task_invalid',
    })
  }

  const now = nowIso()
  const actor = normalizeActor(options.actor)
  const force = options.force === true
  const taskContext = parseTaskContext(task)
  const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
  const claimLinkState = await ensureTaskClaimLink(task)
  const source = String(options.source || 'system').trim() || 'system'

  // 已发货：禁止 prepare / 过期换号把状态打回 waiting_binding
  if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
    return {
      task,
      claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
      token: String(task.primary_claim_token || task.claim_token || ''),
      flow,
    }
  }

  if (
    !force &&
    flow.binding.prepareStatus === 'ready' &&
    flow.binding.vnId > 0 &&
    flow.binding.vnPhone &&
    flow.binding.bindUrl
  ) {
    if (!isKuaishouCloudBindUrlFresh(flow)) {
      return refreshKuaishouCloudTaskBindUrl(task, {
        source: options.source || 'system_refresh_expired_bind_url',
        actor,
        claimLinkState,
      })
    }

    let readyTask = task

    if (normalizeTaskStatus(task.task_status) !== TASK_STATUS.WAITING_BINDING) {
      readyTask =
        (await updateTask(task.id, {
          task_status: TASK_STATUS.WAITING_BINDING,
          claim_token: claimLinkState.token || task.claim_token || '',
          claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
          updated_at: now,
        })) || task
    }

    return {
      task: readyTask,
      claimUrl: claimLinkState.claimUrl,
      token: claimLinkState.token,
      flow,
    }
  }

  // force 重建：旧号可能已被 CT 回收，退号失败直接忽略，下面重新取号
  let previousVnRelease: 'none' | 'returned' | 'already_reclaimed' | 'skipped' = 'none'
  if (force && flow.binding.vnId > 0 && flow.binding.vnKey) {
    previousVnRelease = await tryReleasePreviousVirtualNumber({
      task,
      flow,
      actor,
      source,
      now,
    })
  }

  const excludedSourceKeys = normalizeStringArray(options.excludedSourceKeys)
  const sourceSelectionAttempts = normalizeStringArray(options.sourceSelectionAttempts)
  const sourceCandidateCount = new Set(
    normalizeStringArray([...flow.binding.cloudSourceKeys, flow.binding.resolvedSourceKey]),
  ).size
  const selectedCloud = await selectCloudtentaclesSourceForFulfillment({
    ...flow,
    excludedSourceKeys,
  })
  let retriedOnAnotherSource = false
  try {
    const cloudContext = selectedCloud.context
    const [knapsack, skuList] = await Promise.all([
      getCloudtentaclesKnapsack(cloudContext),
      listCloudtentaclesSku(cloudContext),
    ])
    const resolvedBinding = resolveKuaishouCloudBindingResources(flow, {
      skuItems: Array.isArray(skuList.items) ? skuList.items : [],
      knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
    })
    const vnKeyCandidates = resolveKuaishouCloudVnKeyCandidates({
      flow,
      binding: resolvedBinding,
    })

    if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
      throw createHttpError('当前任务缺少可用 cloud 资源，无法自动准备绑定', {
        statusCode: 409,
        errorCode: 'kuaishou_cloud_missing_binding_config',
      })
    }

    const flowWithResolvedBinding = {
      ...flow,
      binding: {
        ...flow.binding,
        skuId: resolvedBinding.skuId,
        skuName: resolvedBinding.skuName,
        vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
      },
    }

    const deliveryPlan = resolveKuaishouCloudDeliveryPlan(flowWithResolvedBinding, {
      skuItems: Array.isArray(skuList.items) ? skuList.items : [],
      knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
    })
    if (deliveryPlan.items.length === 0) {
      throw createHttpError('当前任务缺少 cloud 发货物品配置', {
        statusCode: 409,
        errorCode: 'kuaishou_cloud_missing_delivery_items',
      })
    }

    const usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0)
    if (!usedKnapsack && !flowWithResolvedBinding.purchase.autoBuyEnabled) {
      throw createHttpError('背包中没有现成库存，且当前配置未开启自动购买', {
        statusCode: 409,
        errorCode: 'kuaishou_cloud_auto_buy_disabled',
      })
    }

    const missingSku = deliveryPlan.items.find((item) => item.missingCount > 0 && !item.skuItem)
    if (missingSku) {
      throw createHttpError(`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`, {
        statusCode: 404,
        errorCode: 'kuaishou_cloud_sku_not_found',
      })
    }

    const purchaseTriggered = false
    const assetBefore = 0
    const assetAfter = 0

    const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
      cloudContext,
      vnKeyCandidates,
      purpose: 'new_fulfillment',
    })
    const defaultRoleSnapshot = await captureKuaishouCloudDefaultRoleSnapshot({
      cloudContext,
      preparedBinding,
      now,
    })

    const nextContext = {
      ...taskContext,
      kuaishouCloudFulfillment: {
        ...flowWithResolvedBinding,
        binding: {
          ...flowWithResolvedBinding.binding,
          resolvedSourceKey: cloudContext.resolvedSourceKey,
          vnKey: preparedBinding.vnKey,
          prepareStatus: 'ready',
          vnId: preparedBinding.vnId,
          vnPhone: preparedBinding.vnPhone,
          bindUrl: preparedBinding.bindUrl,
          bindPreparedAt: now,
          bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
          bindProbeAt: null as null,
          bindProbeStatus: 'pending',
          bindProbeMessage: '',
          roleName: '',
          roleId: '',
        },
        role: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot),
        purchase: {
          ...flowWithResolvedBinding.purchase,
          usedKnapsack,
          purchaseTriggered,
          assetBefore,
          assetAfter,
          items: deliveryPlan.items.map((item) => ({
            cloudSkuId: item.cloudSkuId,
            cloudSkuName: item.cloudSkuName,
            requiredCount: item.quantity,
            knapsackCount: item.knapsackCount,
            purchasedCount: 0,
          })),
          purchaseAt: flowWithResolvedBinding.purchase.purchaseAt,
        },
      },
    }

    const lastErrorNote =
      previousVnRelease === 'already_reclaimed' || previousVnRelease === 'skipped'
        ? '已重新取号并生成绑定链接（旧号可能已被 CT 回收，已忽略退号失败）'
        : force
          ? '已重新取号并生成绑定链接'
          : ''

    const updatedTask = await updateTask(task.id, {
      task_status: TASK_STATUS.WAITING_BINDING,
      user_action_status: 'pending_claim',
      claim_token: claimLinkState.token || task.claim_token || '',
      claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
      role_id: '',
      role_name: '',
      last_error: lastErrorNote,
      context_json: JSON.stringify(nextContext),
      updated_at: now,
    })

    await createTaskEvent(
      task.id,
      'kuaishou_cloud_binding_prepared',
      {
        source,
        force,
        previousVnRelease,
        skuId: flowWithResolvedBinding.binding.skuId,
        skuName: flowWithResolvedBinding.binding.skuName,
        vnKey: preparedBinding.vnKey,
        vnId: preparedBinding.vnId,
        vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
        purchaseTriggered,
        usedKnapsack,
        deliveryItems: deliveryPlan.items.map((item) => ({
          cloudSkuId: item.cloudSkuId,
          cloudSkuName: item.cloudSkuName,
          quantity: item.quantity,
          knapsackCount: item.knapsackCount,
          purchasedCount: 0,
        })),
        resolvedByName: resolvedBinding.resolvedByName,
        accountSelection: {
          ...buildCloudtentaclesSourceSelectionEventDetail(selectedCloud),
          attemptedSourceKeys: [...sourceSelectionAttempts, selectedCloud.sourceKey],
        },
        defaultRoleName: defaultRoleSnapshot.defaultName,
        defaultRoleId: defaultRoleSnapshot.defaultRid,
        defaultRoleCaptureStatus: defaultRoleSnapshot.defaultCaptureStatus,
        actor,
      },
      now,
    )

    return {
      task: updatedTask,
      claimUrl: claimLinkState.claimUrl,
      token: claimLinkState.token,
      flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
      previousVnRelease,
    }
  } catch (error) {
    // Before a VN exists, account-scoped failures are safe to retry on another
    // candidate. Once a VN is acquired, switching accounts would orphan it.
    if (
      flow.binding.vnId <= 0 &&
      isRetryableCloudtentaclesSourceError(error) &&
      excludedSourceKeys.length + 1 < sourceCandidateCount
    ) {
      retriedOnAnotherSource = true
      selectedCloud.release()
      return prepareKuaishouCloudFulfillmentTask(task, {
        ...options,
        excludedSourceKeys: [...excludedSourceKeys, selectedCloud.sourceKey],
        sourceSelectionAttempts: [...sourceSelectionAttempts, selectedCloud.sourceKey],
      })
    }
    throw error
  } finally {
    if (!retriedOnAnotherSource) {
      selectedCloud.release()
    }
  }
}

function isRetryableCloudtentaclesSourceError(error: unknown) {
  const current = error && typeof error === 'object' ? (error as JsonObject) : {}
  const code = String(current.errorCode || current.code || '').trim()
  return (
    code === 'cloudtentacles_vn_quota_cooldown' ||
    (code === 'cloudtentacles_vn_appoint_failed' &&
      String(current.message || '')
        .trim()
        .includes('最多同时占用')) ||
    code === 'cloudtentacles_vn_list_failed' ||
    code === 'cloudtentacles_sku_list_failed' ||
    code === 'cloudtentacles_knapsack_failed' ||
    code === 'cloudtentacles_asset_failed'
  )
}

/** 旧虚拟号 best-effort 释放：已被回收/权限不足时不算失败 */
async function tryReleasePreviousVirtualNumber({
  task,
  flow,
  actor,
  source,
  now,
}: {
  task: TaskRow
  flow: ReturnType<typeof normalizeKuaishouCloudFlow>
  actor: unknown
  source: string
  now: string
}): Promise<'returned' | 'already_reclaimed' | 'skipped'> {
  try {
    const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
      flow.binding.resolvedSourceKey,
      ...flow.binding.cloudSourceKeys,
    ])
    await backCloudtentaclesVirtualNumber({
      ...cloudContext,
      key: flow.binding.vnKey,
      id: flow.binding.vnId,
    })
    await createTaskEvent(
      task.id,
      'kuaishou_cloud_previous_number_returned_before_reprepare',
      {
        source,
        vnId: flow.binding.vnId,
        vnKey: flow.binding.vnKey,
        vnPhoneMasked: maskPhone(flow.binding.vnPhone),
        cloudSourceKey: cloudContext.resolvedSourceKey,
        actor,
      },
      now,
    )
    return 'returned'
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error || '')
    const reclaimed =
      message.includes('权限不足') ||
      message.includes('不存在') ||
      message.includes('已释放') ||
      message.includes('已回收')
    await createTaskEvent(
      task.id,
      reclaimed
        ? 'kuaishou_cloud_previous_number_already_reclaimed'
        : 'kuaishou_cloud_previous_number_release_skipped',
      {
        source,
        vnId: flow.binding.vnId,
        vnKey: flow.binding.vnKey,
        vnPhoneMasked: maskPhone(flow.binding.vnPhone),
        errorMessage: message,
        actor,
      },
      now,
    )
    return reclaimed ? 'already_reclaimed' : 'skipped'
  }
}

/**
 * 刷新绑定链接（不退订单/不重建 claim）：
 * 1. 优先同 vn 调 bind_url 重取链接（不退号）
 * 2. 失败再退号 + 取新号
 * 3. 退号失败可软跳过继续取新号（allowSkipBack，默认 true）
 */
export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
  if (!isKuaishouCloudTask(task)) {
    throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
      statusCode: 409,
      errorCode: 'kuaishou_cloud_task_invalid',
    })
  }

  const now = nowIso()
  const actor = normalizeActor(options.actor)
  const taskContext = parseTaskContext(task)
  const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
  const source = String(options.source || 'system').trim() || 'system'
  // 默认退号失败即中止：避免旧号没退掉又占新号，导致账号号码配额被净泄漏
  const allowSkipBack = options.allowSkipBack === true
  const preferReuseVn = options.preferReuseVn !== false

  if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
    throw createHttpError('当前任务已经发货，不能刷新或更换绑定链接', {
      statusCode: 409,
      errorCode: 'kuaishou_cloud_bind_url_refresh_after_dispatch_forbidden',
    })
  }

  if (!flow.binding.vnId || !flow.binding.vnKey) {
    throw createHttpError('当前任务缺少可刷新绑定链接的虚拟号信息', {
      statusCode: 409,
      errorCode: 'kuaishou_cloud_missing_bind_url_context',
    })
  }

  let cloudContext: JsonObject
  try {
    cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
      flow.binding.resolvedSourceKey,
      ...flow.binding.cloudSourceKeys,
    ])
  } catch (error) {
    throw wrapCloudtentaclesOperationError(error, {
      action: 'resolve_source',
      actionLabel: '解析取号账号',
      sourceKey: flow.binding.resolvedSourceKey || flow.binding.cloudSourceKeys[0],
      vnKey: flow.binding.vnKey,
      vnId: flow.binding.vnId,
      vnPhone: flow.binding.vnPhone,
    })
  }

  const oldVnKey = flow.binding.vnKey
  const oldVnId = flow.binding.vnId
  const oldVnPhone = flow.binding.vnPhone
  const claimLinkState = options.claimLinkState || (await ensureTaskClaimLink(task))

  // 路径 1：复用现有虚拟号，只重取 bindUrl（不退号、不占新号）
  if (preferReuseVn) {
    try {
      const bindUrlResult = await getCloudtentaclesBindUrl({
        ...cloudContext,
        key: oldVnKey,
        id: oldVnId,
      })
      const bindUrl = String(bindUrlResult.bindUrl || '').trim()
      if (bindUrl) {
        const nextContext = {
          ...taskContext,
          kuaishouCloudFulfillment: {
            ...flow,
            binding: {
              ...flow.binding,
              prepareStatus: 'ready',
              resolvedSourceKey: String(
                cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
              ).trim(),
              bindUrl,
              bindPreparedAt: now,
              bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
              bindProbeAt: null as null,
              bindProbeStatus: 'pending',
              bindProbeMessage: '',
            },
          },
        }
        const updatedTask = await updateTask(task.id, {
          task_status:
            normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
              ? task.task_status
              : TASK_STATUS.WAITING_BINDING,
          claim_token: claimLinkState.token || task.claim_token || '',
          claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
          last_error: '',
          context_json: JSON.stringify(nextContext),
          updated_at: now,
        })

        await createTaskEvent(
          task.id,
          'kuaishou_cloud_bind_url_refreshed',
          {
            source,
            mode: 'reuse_existing_vn',
            vnKey: oldVnKey,
            vnId: oldVnId,
            vnPhoneMasked: maskPhone(oldVnPhone),
            cloudSourceKey: cloudContext.resolvedSourceKey,
            actor,
          },
          now,
        )

        return {
          task: updatedTask,
          claimUrl: claimLinkState.claimUrl,
          token: claimLinkState.token,
          flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
          mode: 'reuse_existing_vn',
        }
      }
    } catch (error) {
      // 号码已退/已回收/无权限：直接清空绑定，不再走退号换号（避免反复打上游）
      if (isNumberAlreadyReleasedError(error)) {
        return clearKuaishouCloudStaleBinding(task, {
          taskContext,
          flow,
          now,
          actor,
          source,
          error,
          claimUrl: claimLinkState.claimUrl,
          token: claimLinkState.token || task.claim_token || '',
        })
      }
      // 其他错误走退号换号
    }
  }

  // 路径 2：退旧号 + 取新号
  let oldNumberBackStatus: 'success' | 'skipped' = 'success'
  let oldNumberBackError = ''
  try {
    await backCloudtentaclesVirtualNumber({
      ...cloudContext,
      key: oldVnKey,
      id: oldVnId,
    })
    await createTaskEvent(
      task.id,
      'kuaishou_cloud_expired_bind_number_returned',
      {
        source,
        vnKey: oldVnKey,
        vnId: oldVnId,
        vnPhoneMasked: maskPhone(oldVnPhone),
        cloudSourceKey: cloudContext.resolvedSourceKey,
        actor,
      },
      now,
    )
  } catch (error) {
    // 号码已退/已回收/无权限：直接清空绑定（不再尝试取新号），避免反复打上游
    if (isNumberAlreadyReleasedError(error)) {
      return clearKuaishouCloudStaleBinding(task, {
        taskContext,
        flow,
        now,
        actor,
        source,
        error,
        claimUrl: claimLinkState.claimUrl,
        token: claimLinkState.token || task.claim_token || '',
      })
    }
    oldNumberBackError = error instanceof Error ? error.message : String(error || '退号失败')
    const wrapped = wrapCloudtentaclesOperationError(error, {
      action: 'vn_back',
      actionLabel: '退还过期虚拟号',
      sourceKey: cloudContext.resolvedSourceKey || cloudContext.sourceKey,
      vnKey: oldVnKey,
      vnId: oldVnId,
      vnPhone: oldVnPhone,
    })
    await createTaskEvent(
      task.id,
      'kuaishou_cloud_expired_bind_number_back_failed',
      {
        source,
        vnKey: oldVnKey,
        vnId: oldVnId,
        errorMessage: wrapped.message,
        cloudSourceKey: cloudContext.resolvedSourceKey,
        actor,
      },
      now,
    )
    if (!allowSkipBack) {
      throw wrapped
    }
    oldNumberBackStatus = 'skipped'
  }

  let preparedBinding
  try {
    preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
      cloudContext,
      purpose: 'expired_binding_replacement',
      vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({
        flow,
        binding: flow.binding,
      }),
    })

    if (!String(preparedBinding.bindUrl || '').trim()) {
      throw createHttpError('cloudtentacles 未返回新的绑定链接', {
        statusCode: 502,
        errorCode: 'kuaishou_cloud_empty_bind_url',
      })
    }
  } catch (error) {
    const failedTask = await markKuaishouCloudBindUrlRefreshFailed(task, {
      taskContext,
      flow,
      now,
      actor,
      error,
      oldNumberBackStatus,
      oldNumberBackError,
    })

    return {
      task: failedTask,
      claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
      token: String(task.primary_claim_token || task.claim_token || ''),
      flow: normalizeKuaishouCloudFlow(parseTaskContext(failedTask).kuaishouCloudFulfillment),
    }
  }

  const defaultRoleSnapshot =
    normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
      ? null
      : await captureKuaishouCloudDefaultRoleSnapshot({
          cloudContext,
          preparedBinding,
          now,
        })
  const skipNote =
    oldNumberBackStatus === 'skipped'
      ? `旧号 vnId=${oldVnId} 退还失败已跳过（账号=${cloudContext.resolvedSourceKey}），已换新号绑链`
      : ''
  const nextContext = {
    ...taskContext,
    kuaishouCloudFulfillment: {
      ...flow,
      binding: {
        ...flow.binding,
        prepareStatus: 'ready',
        resolvedSourceKey: String(
          cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
        ).trim(),
        vnKey: preparedBinding.vnKey,
        vnId: preparedBinding.vnId,
        vnPhone: preparedBinding.vnPhone,
        bindUrl: preparedBinding.bindUrl,
        bindPreparedAt: now,
        bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
        bindProbeAt: null as null,
        bindProbeStatus: 'pending',
        bindProbeMessage: '',
        roleName:
          normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
            ? flow.binding.roleName
            : '',
        roleId:
          normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
            ? flow.binding.roleId
            : '',
      },
      role:
        normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
          ? flow.role
          : buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot!),
    },
  }

  const updatedTask = await updateTask(task.id, {
    task_status:
      normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
        ? task.task_status
        : TASK_STATUS.WAITING_BINDING,
    claim_token: claimLinkState.token || task.claim_token || '',
    claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
    role_id:
      normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED ? task.role_id : '',
    role_name:
      normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED ? task.role_name : '',
    last_error: skipNote,
    context_json: JSON.stringify(nextContext),
    updated_at: now,
  })

  await createTaskEvent(
    task.id,
    'kuaishou_cloud_bind_url_refreshed',
    {
      source,
      mode: 'replace_vn',
      oldVnId,
      oldVnPhoneMasked: maskPhone(oldVnPhone),
      vnKey: preparedBinding.vnKey,
      vnId: preparedBinding.vnId,
      vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
      oldNumberBackStatus,
      oldNumberBackError,
      cloudSourceKey: cloudContext.resolvedSourceKey,
      actor,
    },
    now,
  )

  return {
    task: updatedTask,
    claimUrl: claimLinkState.claimUrl,
    token: claimLinkState.token,
    flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
    mode: 'replace_vn',
    oldNumberBackStatus,
  }
}

async function markKuaishouCloudBindUrlRefreshFailed(
  task: TaskRow,
  {
    taskContext,
    flow,
    now,
    actor,
    error,
    oldNumberBackStatus = 'unknown',
    oldNumberBackError = '',
  }: JsonObject = {},
) {
  const errorMessage =
    error instanceof Error ? error.message : String(error || '新绑定链接准备失败')
  const backNote =
    oldNumberBackStatus === 'skipped'
      ? `旧号退还失败（${oldNumberBackError || '未知'}）且`
      : oldNumberBackStatus === 'success'
        ? '旧号码已退还，'
        : ''
  const nextContext = {
    ...taskContext,
    kuaishouCloudFulfillment: {
      ...flow,
      binding: {
        ...flow.binding,
        prepareStatus: 'pending',
        vnId: oldNumberBackStatus === 'success' ? 0 : flow.binding?.vnId || 0,
        vnPhone: oldNumberBackStatus === 'success' ? '' : flow.binding?.vnPhone || '',
        bindUrl: '',
        bindPreparedAt: null,
        bindExpiresAt: null,
        bindProbeAt: now,
        bindProbeStatus: 'refresh_failed',
        bindProbeMessage: errorMessage,
        roleName: '',
        roleId: '',
      },
      role: {
        status: 'pending',
        name: '',
        rid: '',
        refreshedAt: now,
        errorMessage: `绑定链接刷新失败：${backNote}新链接准备失败，请稍后重试或联系客服`,
        rawInfo: null,
      },
    },
  }

  const updatedTask = await updateTask(task.id, {
    task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
    role_id: '',
    role_name: '',
    last_error: `绑定链接刷新失败：${backNote}新链接准备失败：${errorMessage}`,
    context_json: JSON.stringify(nextContext),
    updated_at: now,
  })

  await createTaskEvent(
    task.id,
    'kuaishou_cloud_bind_url_refresh_failed',
    {
      source: 'system_refresh_expired_bind_url',
      errorMessage,
      actor,
    },
    now,
  )

  await notifyKuaishouCloudBindUrlRefreshFailed({
    task: updatedTask,
    errorMessage,
  })

  return updatedTask
}

/** 号码已被上游回收/手动退回/账号换 token 后无操作权限：重试无意义，应清空任务绑定 */
function isNumberAlreadyReleasedError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error || '')
  return (
    message.includes('权限不足') ||
    message.includes('没有权限') ||
    message.includes('不存在') ||
    message.includes('已释放') ||
    message.includes('已回收') ||
    message.includes('已退还')
  )
}

/**
 * 清空任务侧已失效的旧绑定（号码已退/已回收/无权限），任务下次领取时自动重新取号。
 * 所有调用路径（claim 轮询 probe、open-91 交付、回收任务、后台刷新）统一收敛，
 * 避免对失效号码反复打上游。
 */
async function clearKuaishouCloudStaleBinding(
  task: TaskRow,
  {
    taskContext,
    flow,
    now,
    actor,
    source,
    error,
    claimUrl,
    token,
  }: {
    taskContext: JsonObject
    flow: ReturnType<typeof normalizeKuaishouCloudFlow>
    now: string
    actor: unknown
    source: string
    error: unknown
    claimUrl: string
    token: string
  },
) {
  const errorMessage = error instanceof Error ? error.message : String(error || '绑定已失效')
  const oldVnId = flow.binding.vnId
  const oldVnPhone = flow.binding.vnPhone
  const nextContext = {
    ...taskContext,
    kuaishouCloudFulfillment: {
      ...flow,
      binding: {
        ...flow.binding,
        prepareStatus: 'pending',
        vnId: 0,
        vnPhone: '',
        bindUrl: '',
        bindPreparedAt: null,
        bindExpiresAt: null,
        bindProbeAt: now,
        bindProbeStatus: 'stale_cleared',
        bindProbeMessage: errorMessage,
        roleName: '',
        roleId: '',
      },
      role: {
        status: 'pending',
        name: '',
        rid: '',
        refreshedAt: now,
        errorMessage: '旧绑定号码已失效，已清空绑定，将重新取号',
        rawInfo: null,
      },
    },
  }

  const updatedTask = await updateTask(task.id, {
    task_status:
      normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
        ? task.task_status
        : TASK_STATUS.PENDING_BINDING_PREPARE,
    role_id: '',
    role_name: '',
    last_error: `旧绑定号码已失效（${errorMessage}），已清空绑定`,
    context_json: JSON.stringify(nextContext),
    updated_at: now,
  })

  await createTaskEvent(
    task.id,
    'kuaishou_cloud_stale_binding_cleared',
    {
      source,
      oldVnId,
      oldVnPhoneMasked: maskPhone(oldVnPhone),
      cloudSourceKey: flow.binding.resolvedSourceKey || flow.binding.cloudSourceKeys[0] || '',
      errorMessage,
      actor,
    },
    now,
  )

  return {
    task: updatedTask || task,
    claimUrl,
    token,
    flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
    mode: 'stale_cleared',
  }
}
