import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { randomUUID } from 'node:crypto'
import { z } from 'zod/v4'
import { computeNextCronRun, parseCronExpression } from './cron.js'
import { isEnabledInAgentConfig } from './agentConfig.js'

/**
 * @typedef {import('../../../src/Tool.js').Tool} Tool
 * @typedef {import('../../../src/Tool.js').ToolUseContext} ToolUseContext
 * @typedef {import('../../../src/Tool.js').ToolResult} ToolResult
 */

const MAX_TASKS = 50
/** 一次性任务：下次触发距现在不得超过 7 天，否则视为时区/cron 写错 */
const ONE_SHOT_MAX_DELAY_MS = 7 * 24 * 60 * 60 * 1000
const MAX_DELAY_MINUTES = 7 * 24 * 60

type ScheduleTask = {
  id: string
  sessionId: string
  cron: string
  prompt: string
  createdAt: number
  recurring?: boolean
  lastFiredAt?: number
}

type ScheduleFile = {
  version: 1
  tasks: ScheduleTask[]
}

type ScheduleClock = {
  timezone: string
  localDateTime: string
  utcIso: string
}

type ScheduleConfigure = {
  getSessionId: () => string
  /** 返回 agent 级 schedule.json 路径 */
  getScheduleFilePath: () => string
  /** 调度器本地时钟（与 agent 进程 TZ 一致，cron 按此解释） */
  getScheduleClock?: () => ScheduleClock
}

let hooks: ScheduleConfigure | null = null
let cachedHooks: ScheduleConfigure | null = null

/** 宿主注入 sessionId 与 schedule 文件路径 */
export function configure(next: ScheduleConfigure): void {
  hooks = next
  cachedHooks = next
}

export function resetConfigure(): void {
  hooks = null
}

export function ensureConfigured(): void {
  if (cachedHooks) {
    hooks = cachedHooks
  }
}

function requireHooks(): ScheduleConfigure {
  if (!hooks) {
    throw new Error(
      'ScheduleTool: configure() not called (getSessionId / getScheduleFilePath)',
    )
  }
  return hooks
}

function buildLocalScheduleClock(): ScheduleClock {
  const now = new Date()
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
  const y = now.getFullYear()
  const mo = String(now.getMonth() + 1).padStart(2, '0')
  const d = String(now.getDate()).padStart(2, '0')
  const h = String(now.getHours()).padStart(2, '0')
  const mi = String(now.getMinutes()).padStart(2, '0')
  const s = String(now.getSeconds()).padStart(2, '0')
  return {
    timezone,
    localDateTime: `${y}-${mo}-${d}T${h}:${mi}:${s}`,
    utcIso: now.toISOString(),
  }
}

/** 读取调度器本地时钟；未注入时用当前进程时区 */
function readScheduleClock(): ScheduleClock {
  const fromHost = hooks?.getScheduleClock?.()
  if (fromHost) {
    return fromHost
  }
  return buildLocalScheduleClock()
}

/**
 * 按调度器本地时区，把「现在 + delayMinutes」转成 5 段 cron（固定日/月，一次性）。
 */
function cronFromDelayMinutes(delayMinutes: number): string {
  const t = new Date(Date.now() + delayMinutes * 60_000)
  return `${t.getMinutes()} ${t.getHours()} ${t.getDate()} ${t.getMonth() + 1} *`
}

/**
 * 校验一次性 cron：若下次触发超过 7 天，多半是误用 UTC 钟面，拒绝创建。
 */
function validateOneShotCronNextRun(
  cron: string,
  clock: ScheduleClock,
): string | null {
  const fields = parseCronExpression(cron)
  if (!fields) {
    return `Invalid cron expression: ${cron}`
  }
  const now = Date.now()
  const next = computeNextCronRun(fields, new Date(now))
  if (!next) {
    return `Invalid cron expression: ${cron}`
  }
  const delta = next.getTime() - now
  if (delta <= ONE_SHOT_MAX_DELAY_MS) {
    return null
  }
  const days = Math.round(delta / 86_400_000)
  return (
    `One-shot schedule would fire at ${next.toLocaleString()} (${days} days away). ` +
    `Cron minute/hour/day/month must use scheduler local time (timezone=${clock.timezone}, ` +
    `now local=${clock.localDateTime}, utc=${clock.utcIso}). ` +
    `For "in N minutes" use delayMinutes instead of UTC timestamps from messages.`
  )
}

function buildScheduleToolPrompt(): string {
  const clock = readScheduleClock()
  return `# ScheduleTool

## Scheduler clock (cron fields MUST match this local time, NOT UTC from message timestamps)
- timezone: ${clock.timezone}
- now (local): ${clock.localDateTime}
- now (UTC): ${clock.utcIso}

## Actions
- action create (preferred for relative time): { delayMinutes, prompt, recurring?: boolean }
  - delayMinutes: 1..${MAX_DELAY_MINUTES}, fires after N minutes in ${clock.timezone}
- action create (absolute cron): { cron, prompt, recurring?: boolean }
  - cron: 5-field "minute hour day-of-month month day-of-week" in ${clock.timezone} only
- action delete: { id }
- action list: {}

Provide exactly one of cron or delayMinutes on create. Tasks persist under agent schedule.json (bound to current session).`
}

async function readScheduleFile(path: string): Promise<ScheduleTask[]> {
  let raw: string
  try {
    raw = await readFile(path, 'utf8')
  } catch {
    return []
  }
  let parsed: unknown
  try {
    parsed = JSON.parse(raw)
  } catch {
    return []
  }
  if (
    !parsed ||
    typeof parsed !== 'object' ||
    (parsed as ScheduleFile).version !== 1 ||
    !Array.isArray((parsed as ScheduleFile).tasks)
  ) {
    return []
  }
  const out: ScheduleTask[] = []
  for (const t of (parsed as ScheduleFile).tasks) {
    if (
      typeof t.id !== 'string' ||
      typeof t.sessionId !== 'string' ||
      typeof t.cron !== 'string' ||
      typeof t.prompt !== 'string' ||
      typeof t.createdAt !== 'number' ||
      !parseCronExpression(t.cron)
    ) {
      continue
    }
    out.push({
      id: t.id,
      sessionId: t.sessionId,
      cron: t.cron,
      prompt: t.prompt,
      createdAt: t.createdAt,
      ...(t.recurring ? { recurring: true } : {}),
      ...(typeof t.lastFiredAt === 'number' ? { lastFiredAt: t.lastFiredAt } : {}),
    })
  }
  return out
}

async function writeScheduleFile(
  path: string,
  tasks: ScheduleTask[],
): Promise<void> {
  const dir = join(path, '..')
  await mkdir(dir, { recursive: true })
  const body: ScheduleFile = { version: 1, tasks }
  await writeFile(path, `${JSON.stringify(body, null, 2)}\n`, 'utf8')
}

/**
 * 模型调用常把 boolean 写成 "true"/"false" 字符串；仅接受明确可解析的值。
 */
function parseOptionalBoolean(value: unknown): boolean | undefined {
  if (value === undefined || value === null) {
    return undefined
  }
  if (typeof value === 'boolean') {
    return value
  }
  if (typeof value === 'number') {
    return value !== 0
  }
  if (typeof value === 'string') {
    const s = value.trim().toLowerCase()
    if (s === 'true' || s === '1') {
      return true
    }
    if (s === 'false' || s === '0') {
      return false
    }
  }
  throw new Error(
    `recurring must be boolean or "true"/"false", got ${JSON.stringify(value)}`,
  )
}

const optionalRecurringSchema = z.preprocess(
  parseOptionalBoolean,
  z.boolean().optional(),
)

/**
 * 模型常把 delayMinutes 写成 "1" 字符串；仅接受可解析为整数的值。
 */
function parseOptionalDelayMinutes(value: unknown): number | undefined {
  if (value === undefined || value === null) {
    return undefined
  }
  if (typeof value === 'number') {
    if (!Number.isFinite(value) || !Number.isInteger(value)) {
      throw new Error(`delayMinutes must be an integer, got ${value}`)
    }
    return value
  }
  if (typeof value === 'string') {
    const s = value.trim()
    if (s.length === 0) {
      return undefined
    }
    const n = Number(s)
    if (!Number.isFinite(n) || !Number.isInteger(n)) {
      throw new Error(`delayMinutes must be an integer, got ${JSON.stringify(value)}`)
    }
    return n
  }
  throw new Error(
    `delayMinutes must be a number or numeric string, got ${JSON.stringify(value)}`,
  )
}

const optionalDelayMinutesSchema = z.preprocess(
  parseOptionalDelayMinutes,
  z.number().int().min(1).max(MAX_DELAY_MINUTES).optional(),
)

async function writeResultFile(
  toolResultsDir: string,
  payload: unknown,
): Promise<string> {
  await mkdir(toolResultsDir, { recursive: true })
  const filePath = join(
    toolResultsDir,
    `schedule-tool-${Date.now()}.json`,
  )
  await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
  return filePath
}

const createActionSchema = z
  .object({
    action: z.literal('create'),
    prompt: z.string().min(1),
    recurring: optionalRecurringSchema,
    cron: z.string().min(1).optional(),
    delayMinutes: optionalDelayMinutesSchema,
  })
  .superRefine((v, ctx) => {
    const hasCron = typeof v.cron === 'string' && v.cron.length > 0
    const hasDelay = v.delayMinutes !== undefined
    if (hasCron === hasDelay) {
      ctx.addIssue({
        code: 'custom',
        message: 'create requires exactly one of cron or delayMinutes',
      })
    }
  })

const inputSchema = z.discriminatedUnion('action', [
  createActionSchema,
  z.object({
    action: z.literal('delete'),
    id: z.string().min(1),
  }),
  z.object({
    action: z.literal('list'),
  }),
])

type ScheduleInput = z.infer<typeof inputSchema>

/**
 * @type {Tool}
 */
export const ScheduleTool = {
  name: 'ScheduleTool',
  description: async () =>
    'Create, delete, or list cron-scheduled prompts for the current WebSocket session (persisted; fired when WS is connected).',
  listInterfaces: [
    { name: 'create', purpose: '创建定时或延时执行的 prompt 任务' },
    { name: 'delete', purpose: '删除指定调度任务' },
    { name: 'list', purpose: '列出当前 session 的调度任务' },
  ],
  prompt: async () => buildScheduleToolPrompt(),
  inputSchema,
  maxResultSizeChars: 50_000,
  isEnabled: () => true,
  isReadOnly: (input: ScheduleInput) => input.action === 'list',
  /**
   * create/delete 会读写 session 调度文件，必须串行，避免并行 RMW 丢任务。
   * list 只读，可与其它只读工具并行。
   */
  isConcurrencySafe: (input: ScheduleInput) => input.action === 'list',
  isDestructive: () => false,
  /**
   * 用户已通过对话表达设提醒意图；工具仅写 session 级 schedule 文件，无需二次弹窗。
   * 并行 create 时若走 ask，UI 只展示一条 permission 会导致另一条 120s 超时。
   */
  checkPermissions: async (input: ScheduleInput) => ({
    behavior: 'allow' as const,
    updatedInput: input,
  }),
  validateInput: async (input: ScheduleInput) => {
    if (input.action !== 'create') {
      return { result: true }
    }
    if (input.delayMinutes !== undefined) {
      return { result: true }
    }
    const cron = input.cron
    if (!cron || !parseCronExpression(cron)) {
      return {
        result: false,
        message: `Invalid cron expression: ${cron ?? '(missing)'}`,
        errorCode: 0,
      }
    }
    if (input.recurring !== true) {
      const clock = readScheduleClock()
      const err = validateOneShotCronNextRun(cron, clock)
      if (err) {
        return { result: false, message: err, errorCode: 0 }
      }
    }
    return { result: true }
  },
  call: async (input: ScheduleInput, ctx: import('../../../src/Tool.js').ToolUseContext) => {
    const { getSessionId, getScheduleFilePath } = requireHooks()
    const sessionId = getSessionId()
    const schedulePath = getScheduleFilePath()
    const toolResultsDir =
      ctx.toolResultsDir ?? join(process.cwd(), '.ws-agents', 'tool-results')

    if (input.action === 'list') {
      const allTasks = await readScheduleFile(schedulePath)
      const tasks = allTasks.filter(t => t.sessionId === sessionId)
      const resultFile = await writeResultFile(toolResultsDir, {
        action: 'list',
        sessionId,
        tasks,
      })
      return {
        data: { action: 'list', sessionId, resultFile, tasks },
      }
    }

    if (input.action === 'delete') {
      const allTasks = await readScheduleFile(schedulePath)
      const remaining = allTasks.filter(
        t => !(t.id === input.id && t.sessionId === sessionId),
      )
      if (remaining.length === allTasks.length) {
        throw new Error(`Schedule task not found: ${input.id}`)
      }
      await writeScheduleFile(schedulePath, remaining)
      const resultFile = await writeResultFile(toolResultsDir, {
        action: 'delete',
        id: input.id,
        removed: 1,
      })
      return {
        data: { action: 'delete', id: input.id, resultFile, removed: 1 },
      }
    }

    const allTasks = await readScheduleFile(schedulePath)
    if (allTasks.length >= MAX_TASKS) {
      throw new Error(`Too many scheduled tasks (max ${MAX_TASKS})`)
    }
    const id = randomUUID().slice(0, 8)
    const recurring = input.recurring === true
    const cron =
      input.delayMinutes !== undefined
        ? cronFromDelayMinutes(input.delayMinutes)
        : input.cron!
    const clock = readScheduleClock()
    allTasks.push({
      id,
      sessionId,
      cron,
      prompt: input.prompt,
      createdAt: Date.now(),
      ...(recurring ? { recurring: true } : {}),
    })
    await writeScheduleFile(schedulePath, allTasks)
    const resultFile = await writeResultFile(toolResultsDir, {
      action: 'create',
      id,
      cron,
      recurring,
      ...(input.delayMinutes !== undefined
        ? { delayMinutes: input.delayMinutes }
        : {}),
      scheduleClock: clock,
    })
    return {
      data: {
        action: 'create',
        id,
        cron,
        recurring,
        ...(input.delayMinutes !== undefined
          ? { delayMinutes: input.delayMinutes }
          : {}),
        scheduleClock: clock,
        resultFile,
      },
    }
  },
  interruptBehavior: () => 'cancel',
  renderToolUseMessage: (input: ScheduleInput) =>
    `ScheduleTool ${input.action}`,
  renderToolResultMessage: (output: unknown) => {
    if (output && typeof output === 'object' && (output as { isError?: boolean }).isError) {
      return `ScheduleTool failed`
    }
    return `ScheduleTool done`
  },
  mapToolResultToToolResultBlockParam: (
    output: unknown,
    toolUseID: string,
  ) => {
    const contentStr =
      typeof output === 'string' ? output : JSON.stringify(output, null, 2)
    if (output && typeof output === 'object' && (output as { isError?: boolean }).isError) {
      return {
        type: 'tool_result' as const,
        tool_use_id: toolUseID,
        content: `<tool_use_error>${contentStr}</tool_use_error>`,
        is_error: true,
      }
    }
    return {
      type: 'tool_result' as const,
      tool_use_id: toolUseID,
      content: contentStr,
    }
  },
  toAutoClassifierInput: () => '',
}

export default ScheduleTool

export type ScheduleToolWsInitContext = {
  agentHomeDir: string
  operWorkspaceDir: string
  sessionId?: string
  agent: {
    tools: string[]
    disallowedTools?: string[]
  }
}

/** 会话初始化：按 agent 白名单与 schedule.json 路径准备 ScheduleTool */
export async function initWsSession(
  ctx: ScheduleToolWsInitContext,
): Promise<{ state: 'ready' } | { state: 'skipped'; reason: string }> {
  if (!isEnabledInAgentConfig(ctx.agent)) {
    resetConfigure()
    return { state: 'skipped', reason: 'ScheduleTool disabled in agent config' }
  }
  const sid = ctx.sessionId?.trim() ?? ''
  configure({
    getSessionId: () => sid,
    getScheduleFilePath: () => join(ctx.agentHomeDir, 'schedule.json'),
    getScheduleClock: buildLocalScheduleClock,
  })
  return { state: 'ready' }
}
