/**
 * 内部短链服务。
 *
 * 注意（2026-07 起）：
 * - 领取主路径已统一为本站 `/#/claim/{token}`，feifei 不再为 claim 生成 shortLink。
 * - 本模块仅保留：历史 `/s/{code}` 解析、其它潜在内部用途。
 * - 新业务不要再依赖 shortLink 作为对外交付链接。
 */
import crypto from 'node:crypto'

import { runtimeConfig } from '../../config/runtime.js'
import {
  createShortLink,
  findShortLinkByCode,
  markShortLinkVisited,
} from '../../repositories/short-link-repo.js'
import { createHttpError } from '../../utils/http.js'
import { addHours, nowIso } from '../../utils/time.js'
import type { ShortLinkRow } from '../../types/repository/rows.js'

const SHORT_LINK_CODE_LENGTH = 10
const SHORT_LINK_MAX_CREATE_ATTEMPTS = 5
const SHORT_LINK_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const DEFAULT_SHORT_LINK_TTL_HOURS = 24 * 30

/** @deprecated feifei claim 已不再使用；保留常量仅兼容历史数据 */
export const SHORT_LINK_SOURCE_KUAISHOU_FEIFEI_CLAIM = 'kuaishou_feifei_claim'

export type ShortLinkPayload = {
  code: string
  url: string
  targetUrl: string
  expiresAt: string | null
  createdAt?: string
}

export async function ensureShortLinkForTarget(
  targetUrl: string,
  options: {
    source?: string
    taskId?: number | string | null
    expiresAt?: string | null
  } = {},
): Promise<ShortLinkPayload> {
  const normalizedTargetUrl = normalizeTargetUrl(targetUrl)
  const source = String(options.source || '').trim()
  if (source === SHORT_LINK_SOURCE_KUAISHOU_FEIFEI_CLAIM) {
    console.warn(
      '[short-link] feifei claim 已不再使用 shortLink 作为对外交付；请使用本站 /#/claim/{token} 并在 H5 拼 uid',
    )
  }
  const targetUrlHash = hashTargetUrl(normalizedTargetUrl)
  const now = nowIso()
  const expiresAt =
    options.expiresAt !== undefined
      ? normalizeNullableIso(options.expiresAt)
      : addHours(now, DEFAULT_SHORT_LINK_TTL_HOURS)

  for (let attempt = 1; attempt <= SHORT_LINK_MAX_CREATE_ATTEMPTS; attempt += 1) {
    try {
      const row = await createShortLink({
        code: generateShortLinkCode(),
        targetUrl: normalizedTargetUrl,
        targetUrlHash,
        source,
        taskId: options.taskId ?? null,
        expiresAt,
        createdAt: now,
        updatedAt: now,
      })

      if (!row) {
        break
      }

      return mapShortLinkPayload(row)
    } catch (error) {
      if (attempt < SHORT_LINK_MAX_CREATE_ATTEMPTS && isShortLinkCodeConflict(error)) {
        continue
      }

      throw error
    }
  }

  throw createHttpError('短链创建失败', {
    statusCode: 500,
    errorCode: 'short_link_create_failed',
  })
}

export async function resolveShortLinkTarget(code: string): Promise<ShortLinkRow> {
  const normalizedCode = normalizeShortLinkCode(code)
  const row = await findShortLinkByCode(normalizedCode)

  if (!row) {
    throw createHttpError('短链不存在', {
      statusCode: 404,
      errorCode: 'short_link_not_found',
    })
  }

  if (isShortLinkExpired(row)) {
    throw createHttpError('短链已过期', {
      statusCode: 410,
      errorCode: 'short_link_expired',
    })
  }

  await markShortLinkVisited(row.code, nowIso())
  return row
}

export function buildShortLinkUrl(code: string): string {
  const normalizedCode = normalizeShortLinkCode(code)
  const baseUrl = resolveShortLinkBaseUrl()
  return `${baseUrl.replace(/\/+$/, '')}/s/${normalizedCode}`
}

export function normalizeShortLinkPayload(value: unknown): ShortLinkPayload | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null
  }

  const source = value as Record<string, unknown>
  const code = String(source.code || '').trim()
  const url = String(source.url || '').trim()
  const targetUrl = String(source.targetUrl || '').trim()
  if (!code || !url || !targetUrl) {
    return null
  }

  const payload: ShortLinkPayload = {
    code,
    url,
    targetUrl,
    expiresAt: normalizeNullableIso(source.expiresAt),
  }
  const createdAt = String(source.createdAt || '').trim()
  if (createdAt) {
    payload.createdAt = createdAt
  }

  return payload
}

function mapShortLinkPayload(row: ShortLinkRow): ShortLinkPayload {
  return {
    code: row.code,
    url: buildShortLinkUrl(row.code),
    targetUrl: row.target_url,
    expiresAt: row.expires_at || null,
    createdAt: row.created_at,
  }
}

function normalizeTargetUrl(value: string): string {
  const text = String(value || '').trim()
  if (!text) {
    throw createHttpError('短链目标地址不能为空', {
      statusCode: 400,
      errorCode: 'short_link_target_required',
    })
  }

  let url: URL
  try {
    url = new URL(text)
  } catch {
    throw createHttpError('短链目标地址无效', {
      statusCode: 400,
      errorCode: 'short_link_target_invalid',
    })
  }

  if (!['http:', 'https:'].includes(url.protocol)) {
    throw createHttpError('短链目标地址只支持 HTTP/HTTPS', {
      statusCode: 400,
      errorCode: 'short_link_target_protocol_invalid',
    })
  }

  return url.toString()
}

function resolveShortLinkBaseUrl(): string {
  const claimBaseUrl = String(runtimeConfig.orders?.claimBaseUrl || '').trim()
  if (!claimBaseUrl) {
    return ''
  }

  try {
    const url = new URL(claimBaseUrl)
    return url.origin
  } catch {
    return claimBaseUrl.replace(/#.*$/, '').replace(/\/+$/, '')
  }
}

function normalizeShortLinkCode(value: unknown): string {
  const code = String(value || '').trim()
  if (!/^[0-9A-Za-z_-]{4,64}$/.test(code)) {
    throw createHttpError('短链编码无效', {
      statusCode: 400,
      errorCode: 'short_link_code_invalid',
    })
  }

  return code
}

function generateShortLinkCode(): string {
  const bytes = crypto.randomBytes(SHORT_LINK_CODE_LENGTH)
  let output = ''
  for (const byte of bytes) {
    output += SHORT_LINK_ALPHABET[byte % SHORT_LINK_ALPHABET.length] || '0'
  }
  return output
}

function hashTargetUrl(targetUrl: string): string {
  return crypto.createHash('sha256').update(targetUrl, 'utf8').digest('hex')
}

function normalizeNullableIso(value: unknown): string | null {
  const text = String(value || '').trim()
  if (!text) {
    return null
  }

  const timestamp = Date.parse(text)
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null
}

function isShortLinkExpired(row: ShortLinkRow): boolean {
  if (!row.expires_at) {
    return false
  }

  const timestamp = Date.parse(row.expires_at)
  return Number.isFinite(timestamp) && timestamp <= Date.now()
}

function isShortLinkCodeConflict(error: unknown): boolean {
  const current =
    error && typeof error === 'object'
      ? (error as { code?: unknown; constraint?: unknown; detail?: unknown })
      : {}
  if (String(current.code || '') !== '23505') {
    return false
  }

  const marker = `${String(current.constraint || '')} ${String(current.detail || '')}`
  return marker.includes('short_links_code') || marker.includes('code')
}
