import { z } from 'zod'
import {
  weixinBindingStatusSchema,
  weixinVerificationInputSchema
} from '../../shared/weixin-channel-contracts'
import {
  channelAttachmentsSchema
} from '../../shared/channel-contracts'

export const WECHAT_SIDECAR_MAX_TEXT_LENGTH = 8_000
export const WECHAT_SIDECAR_MAX_QR_PAYLOAD_LENGTH = 4_096
export const WECHAT_SIDECAR_MAX_QR_TTL_MS = 5 * 60 * 1_000
export const WECHAT_SIDECAR_PROTOCOL_VERSION = 2

function containsControlCharacter(value: string): boolean {
  for (const character of value) {
    const code = character.codePointAt(0)
    if (code !== undefined && (code <= 31 || code === 127)) {
      return true
    }
  }
  return false
}

function containsWhitespaceOrControlCharacter(value: string): boolean {
  for (const character of value) {
    if (
      character.trim() === '' ||
      containsControlCharacter(character)
    ) {
      return true
    }
  }
  return false
}

const identifierSchema = z
  .string()
  .min(1)
  .max(256)
  .refine((value) => !containsWhitespaceOrControlCharacter(value))

const textSchema = z
  .string()
  .min(1)
  .max(WECHAT_SIDECAR_MAX_TEXT_LENGTH)

export const wechatSidecarStatusSchema = weixinBindingStatusSchema

export type WechatSidecarStatus = z.infer<
  typeof wechatSidecarStatusSchema
>

export const wechatSidecarStatusMessageSchema = z
  .object({
    type: z.literal('status'),
    status: wechatSidecarStatusSchema,
    detail: z.string().min(1).max(512).optional()
  })
  .strict()

export const wechatSidecarQrMessageSchema = z
  .object({
    type: z.literal('qr'),
    qrId: identifierSchema,
    payload: z
      .string()
      .min(1)
      .max(WECHAT_SIDECAR_MAX_QR_PAYLOAD_LENGTH)
      .refine((value) => !containsControlCharacter(value)),
    expiresAt: z.string().datetime({ offset: true })
  })
  .strict()

export const wechatSidecarInboundMessageSchema = z
  .object({
    type: z.literal('inbound_message'),
    eventId: identifierSchema,
    senderId: identifierSchema,
    conversationId: identifierSchema,
    text: z.string().max(WECHAT_SIDECAR_MAX_TEXT_LENGTH),
    attachments: channelAttachmentsSchema.optional(),
    attachmentError: z.string().trim().min(1).max(512).optional()
  })
  .strict()
  .superRefine((message, context) => {
    if (
      message.text.trim().length === 0 &&
      !message.attachments?.length &&
      !message.attachmentError
    ) {
      context.addIssue({
        code: 'custom',
        path: ['text'],
        message: '消息内容不能为空'
      })
    }
  })

export const wechatSidecarVerificationRequiredMessageSchema = z
  .object({
    type: z.literal('verification_required'),
    prompt: z.string().trim().min(1).max(256)
  })
  .strict()

export const wechatSidecarConnectedMessageSchema = z
  .object({
    type: z.literal('connected'),
    accountId: identifierSchema,
    userId: identifierSchema
  })
  .strict()

export const wechatSidecarReplyResultMessageSchema = z
  .object({
    type: z.literal('reply_result'),
    replyId: identifierSchema,
    ok: z.boolean(),
    error: z.string().trim().min(1).max(512).optional()
  })
  .strict()
  .superRefine((result, context) => {
    if (result.ok === (result.error !== undefined)) {
      context.addIssue({
        code: 'custom',
        path: ['error'],
        message: result.ok
          ? '成功回复不能包含错误'
          : '失败回复必须包含错误'
      })
    }
  })

export const wechatSidecarReplyCommandSchema = z
  .object({
    type: z.literal('reply'),
    replyId: identifierSchema,
    inReplyToEventId: identifierSchema,
    conversationId: identifierSchema,
    text: textSchema,
    attachments: channelAttachmentsSchema.optional()
  })
  .strict()

export const wechatSidecarCancelReplyCommandSchema = z
  .object({
    type: z.literal('cancel_reply'),
    replyId: identifierSchema
  })
  .strict()

export const wechatSidecarMessageSchema = z.discriminatedUnion('type', [
  wechatSidecarStatusMessageSchema,
  wechatSidecarQrMessageSchema,
  wechatSidecarInboundMessageSchema,
  wechatSidecarVerificationRequiredMessageSchema,
  wechatSidecarConnectedMessageSchema,
  wechatSidecarReplyResultMessageSchema
])

export type WechatSidecarMessage = z.infer<
  typeof wechatSidecarMessageSchema
>
export type WechatSidecarQrMessage = z.infer<
  typeof wechatSidecarQrMessageSchema
>

export const wechatSidecarStartLoginCommandSchema = z
  .object({
    type: z.literal('start_login')
  })
  .strict()

export const wechatSidecarSubmitVerificationCommandSchema = z
  .object({
    type: z.literal('submit_verification'),
    code: weixinVerificationInputSchema.shape.code
  })
  .strict()

export const wechatSidecarDisconnectCommandSchema = z
  .object({
    type: z.literal('disconnect')
  })
  .strict()

export const wechatSidecarShutdownCommandSchema = z
  .object({
    type: z.literal('shutdown')
  })
  .strict()

export const wechatSidecarCommandSchema = z.discriminatedUnion('type', [
  wechatSidecarStartLoginCommandSchema,
  wechatSidecarSubmitVerificationCommandSchema,
  wechatSidecarReplyCommandSchema,
  wechatSidecarCancelReplyCommandSchema,
  wechatSidecarDisconnectCommandSchema,
  wechatSidecarShutdownCommandSchema
])
export type WechatSidecarCommand = z.infer<
  typeof wechatSidecarCommandSchema
>

export const wechatSidecarStartAccountCommandSchema = z
  .object({
    type: z.literal('start_account'),
    accountId: identifierSchema,
    userId: identifierSchema,
    baseUrl: z.string().url().max(2_048),
    token: z.string().trim().min(1).max(4_096)
  })
  .strict()

export const wechatSidecarCredentialMessageSchema = z
  .object({
    type: z.literal('credential'),
    accountId: identifierSchema,
    userId: identifierSchema,
    baseUrl: z.string().url().max(2_048),
    token: z.string().trim().min(1).max(4_096)
  })
  .strict()

export type WechatSidecarStartAccountCommand = z.infer<
  typeof wechatSidecarStartAccountCommandSchema
>
export type WechatSidecarCredentialMessage = z.infer<
  typeof wechatSidecarCredentialMessageSchema
>

const allowedTransitions: Readonly<
  Record<WechatSidecarStatus, ReadonlySet<WechatSidecarStatus>>
> = {
  stopped: new Set(['stopped', 'starting']),
  starting: new Set(['starting', 'pending', 'failed', 'stopped']),
  pending: new Set([
    'pending',
    'scanned',
    'expired',
    'failed',
    'stopped'
  ]),
  scanned: new Set([
    'scanned',
    'verification_required',
    'connected',
    'expired',
    'failed',
    'stopped'
  ]),
  verification_required: new Set([
    'verification_required',
    'scanned',
    'expired',
    'failed',
    'stopped'
  ]),
  connected: new Set(['connected', 'failed', 'stopped']),
  expired: new Set(['expired', 'starting', 'stopped']),
  failed: new Set(['failed', 'starting', 'stopped'])
}

export type WechatQrStateSnapshot = {
  status: WechatSidecarStatus
  qr?: WechatSidecarQrMessage
}

export class WechatQrStateMachine {
  private status: WechatSidecarStatus = 'stopped'
  private qr?: WechatSidecarQrMessage

  snapshot(): WechatQrStateSnapshot {
    return {
      status: this.status,
      ...(this.qr ? { qr: { ...this.qr } } : {})
    }
  }

  transition(
    next: WechatSidecarStatus,
    now = Date.now()
  ): WechatQrStateSnapshot {
    this.assertTimestamp(now)
    this.expire(now)

    if (!allowedTransitions[this.status].has(next)) {
      throw new Error(
        `非法的微信扫码状态转换：${this.status} -> ${next}`
      )
    }
    if (
      next === 'scanned' &&
      (!this.qr || Date.parse(this.qr.expiresAt) <= now)
    ) {
      throw new Error('无法扫描已过期或不存在的二维码')
    }

    this.status = next
    if (
      next === 'stopped' ||
      next === 'starting' ||
      next === 'connected' ||
      next === 'expired' ||
      next === 'failed'
    ) {
      this.qr = undefined
    }
    return this.snapshot()
  }

  setQr(input: unknown, now = Date.now()): WechatQrStateSnapshot {
    this.assertTimestamp(now)
    this.expire(now)
    if (this.status !== 'pending') {
      throw new Error('仅等待扫码状态可以接收二维码')
    }

    const qr = wechatSidecarQrMessageSchema.parse(input)
    const expiresAt = Date.parse(qr.expiresAt)
    if (
      !Number.isFinite(expiresAt) ||
      expiresAt <= now ||
      expiresAt - now > WECHAT_SIDECAR_MAX_QR_TTL_MS
    ) {
      throw new Error('二维码有效期无效')
    }
    this.qr = qr
    return this.snapshot()
  }

  expire(now = Date.now()): boolean {
    this.assertTimestamp(now)
    if (
      (this.status === 'pending' ||
        this.status === 'scanned' ||
        this.status === 'verification_required') &&
      this.qr &&
      Date.parse(this.qr.expiresAt) <= now
    ) {
      this.status = 'expired'
      this.qr = undefined
      return true
    }
    return false
  }

  private assertTimestamp(now: number): void {
    if (!Number.isFinite(now) || now < 0) {
      throw new Error('状态机时间无效')
    }
  }
}
