import {
  completeSimple,
  streamSimple,
  type Api as PiApi,
  type Model as PiModel,
  type TextContent as PiTextContent,
} from '@earendil-works/pi-ai'
import type {
  AnalysisPlanIntent,
  AnalysisPlanSummary,
  AnalysisPlanner,
  PlanContentBlock,
  PlannerInput,
} from './planning-types'
import { formatDataSnapshotForPlanner } from './data-snapshot'

export type PlannerCompletionResult = string | { text: string }

export interface PlannerStreamCallbacks {
  onPlanDelta: (delta: string) => void
  onThinkingDelta?: (delta: string) => void
  onThinkingEnd?: (durationMs?: number) => void
  onValidationDelta?: (delta: string) => void
  onValidationEnd?: (durationMs?: number) => void
}

export interface CreateAnalysisPlannerOptions {
  piModel?: PiModel<PiApi>
  apiKey?: string
  complete?: (prompt: string, signal?: AbortSignal) => Promise<PlannerCompletionResult> | PlannerCompletionResult
  stream?: (prompt: string, callbacks: PlannerStreamCallbacks, signal?: AbortSignal) => Promise<PlannerCompletionResult>
  onPlanDelta?: (delta: string) => void
  onThinkingDelta?: (delta: string) => void
  onThinkingEnd?: (durationMs?: number) => void
  onValidationDelta?: (delta: string) => void
  onValidationEnd?: (durationMs?: number) => void
  maxTokens?: number
  temperature?: number
}

const PLAN_INTENTS: readonly AnalysisPlanIntent[] = [
  'summary',
  'trend',
  'relationship',
  'search',
  'comparison',
  'evidence',
  'mixed',
]
const STREAM_MARKER_HOLDBACK_CHARS = Math.max('</draft>'.length, '</plan>'.length, '<json>'.length) - 1

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function isIntent(value: unknown): value is AnalysisPlanIntent {
  return typeof value === 'string' && PLAN_INTENTS.includes(value as AnalysisPlanIntent)
}

function normalizeString(value: unknown): string | null {
  if (typeof value !== 'string') return null
  const text = value.trim()
  return text.length > 0 ? text : null
}

function extractJsonObject(text: string): string {
  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)
  if (fenced?.[1]) return fenced[1].trim()
  const start = text.indexOf('{')
  const end = text.lastIndexOf('}')
  if (start >= 0 && end > start) return text.slice(start, end + 1)
  return text.trim()
}

function parsePlanJson(rawText: string, availableTools: Set<string>): AnalysisPlanSummary | null {
  try {
    const parsed = JSON.parse(extractJsonObject(rawText)) as unknown
    if (!isRecord(parsed)) return null
    const title = normalizeString(parsed.title)
    if (!title || !isIntent(parsed.intent) || !Array.isArray(parsed.steps)) return null

    // 中文注释：Planner 输出来自 LLM，必须在进入 Agent guidance 前做结构约束；
    // 这里裁剪长度并过滤不存在的工具，避免把幻觉工具名注入后续执行上下文。
    const steps = parsed.steps
      .filter(isRecord)
      .map((step) => {
        const goal = normalizeString(step.goal)
        const evidenceNeeded = normalizeString(step.evidenceNeeded)
        if (!goal || !evidenceNeeded) return null
        const suggestedTools = Array.isArray(step.suggestedTools)
          ? step.suggestedTools.filter((tool): tool is string => typeof tool === 'string' && availableTools.has(tool))
          : []
        return { goal, suggestedTools: Array.from(new Set(suggestedTools)), evidenceNeeded }
      })
      .filter((step): step is NonNullable<typeof step> => step !== null)
      .slice(0, 5)

    if (steps.length === 0) return null

    const successCriteria = Array.isArray(parsed.successCriteria)
      ? parsed.successCriteria
          .map(normalizeString)
          .filter((item): item is string => item !== null)
          .slice(0, 5)
      : []
    if (successCriteria.length === 0) return null

    return {
      version: 1,
      title,
      route: 'planned_execution',
      intent: parsed.intent,
      steps,
      successCriteria,
    }
  } catch {
    return null
  }
}

function resultToText(result: PlannerCompletionResult): string {
  return typeof result === 'string' ? result : result.text
}

function buildPlannerPrompt(input: PlannerInput): string {
  const dataSnapshot = formatDataSnapshotForPlanner(input.dataSnapshot)
  const availableCapabilities =
    input.availableCapabilities && input.availableCapabilities.length > 0
      ? input.availableCapabilities
          .map((capability) => {
            const tools = capability.tools.join(', ') || 'none'
            return `  - ${capability.id} (${capability.label}; tools: ${tools}): ${capability.guidance}`
          })
          .join('\n')
      : '(none)'

  return `Create a concise user-visible analysis plan for a ChatLab planned_execution request.

Return exactly three blocks:
<draft>
A brief user-visible planning note in the user's locale. Keep it high-level; do not reveal hidden chain-of-thought.
</draft>
<plan>
A concise user-facing analysis approach in the user's locale. Do not include suggested tools, evidence fields, or success criteria; those details belong in <json>.
</plan>
<json>
{
  "title": "short title",
  "intent": "summary|trend|relationship|search|comparison|evidence|mixed",
  "steps": [
    {"goal": "what to investigate", "suggestedTools": ["tool_name"], "evidenceNeeded": "what evidence is needed"}
  ],
  "successCriteria": ["criterion"]
}
</json>

Limits:
- Max 5 steps.
- Max 5 successCriteria.
- suggestedTools must come only from availableTools.
- If availableCapabilities are listed, you may plan steps that use their tools when the capability helps the user.
- Do not reveal hidden chain-of-thought; draft and plan are user-readable summaries only.
- The plan is soft guidance, not a mandatory execution script.

Planning strategy:
- For open-ended topic, community profile, influence, interaction-pattern, or multi-dimensional retrospective analysis, first plan a lightweight reconnaissance step to build a topic/member activity map before drawing conclusions.
- Prefer segment summaries, keyword frequency, time distribution, member activity, and representative message retrieval when available.
- Do not create a dedicated step whose only purpose is finding max timestamp or confirming latest message time.
- Use intent "evidence" for historical fact / occurrence / count / "did we ever" / proof-from-chat questions that need an evidence chain or a counted/conservative conclusion. When the intent is "evidence" and retrieve_chat_evidence is in availableTools, suggest exactly ["retrieve_chat_evidence"] for that step instead of search_messages or semantic_search_current_chat, so the model does not pick only one low-level retrieval path.

Context:
- locale: ${input.locale}
- chatType: ${input.chatType}
- availableTools: ${input.availableTools.join(', ') || '(none)'}
- availableCapabilities:
${availableCapabilities}
- dataSnapshot: ${dataSnapshot}
- assistantSummary: ${input.assistantSummary ?? '(none)'}
- skillSummary: ${input.skillSummary ?? '(none)'}
- recentIntentSummary: ${input.recentIntentSummary ?? '(none)'}

User request:
${input.userMessage}`
}

export function createAnalysisPlanner(options: CreateAnalysisPlannerOptions): AnalysisPlanner {
  return async (input, signal) => {
    try {
      const prompt = buildPlannerPrompt(input)
      const callbacks: PlannerStreamCallbacks = {
        onPlanDelta: options.onPlanDelta ?? (() => {}),
        onThinkingDelta: options.onThinkingDelta,
        onThinkingEnd: options.onThinkingEnd,
        onValidationDelta: options.onValidationDelta,
        onValidationEnd: options.onValidationEnd,
      }
      const rawResult = options.stream
        ? await options.stream(prompt, callbacks, signal)
        : options.complete
          ? await options.complete(prompt, signal)
          : options.onPlanDelta
            ? await streamWithPiAi(prompt, options, signal)
            : await completeWithPiAi(prompt, options, signal)
      return parsePlanJson(resultToText(rawResult), new Set(input.availableTools))
    } catch {
      return null
    }
  }
}

export function createPlanContentBlock(
  plan: AnalysisPlanSummary,
  status: PlanContentBlock['status'] = 'created'
): PlanContentBlock {
  return {
    type: 'plan',
    version: 1,
    status,
    plan,
  }
}

export function buildPlanGuidance(plan: AnalysisPlanSummary): string {
  const steps = plan.steps
    .map((step, index) => {
      const tools = step.suggestedTools.length > 0 ? step.suggestedTools.join(', ') : 'none'
      return `${index + 1}. ${step.goal}\n   Evidence needed: ${step.evidenceNeeded}\n   Suggested tools: ${tools}`
    })
    .join('\n')
  const successCriteria = plan.successCriteria.map((item) => `- ${item}`).join('\n')

  return `A suggested analysis plan is available below. Use it as guidance when helpful, but do not follow it mechanically if the user's question can be answered more directly.

Plan title: ${plan.title}
Intent: ${plan.intent}

Steps:
${steps}

Success criteria:
${successCriteria}`
}

async function completeWithPiAi(
  prompt: string,
  options: CreateAnalysisPlannerOptions,
  signal?: AbortSignal
): Promise<PlannerCompletionResult> {
  if (!options.piModel || !options.apiKey) {
    throw new Error('Planner requires piModel and apiKey')
  }

  const result = await completeSimple(
    options.piModel,
    {
      systemPrompt: 'You are a strict planner for ChatLab analysis. Return the requested blocks only.',
      messages: [{ role: 'user', content: [{ type: 'text', text: prompt }], timestamp: Date.now() }] as any,
    },
    {
      apiKey: options.apiKey,
      maxTokens: options.maxTokens ?? 900,
      temperature: options.temperature ?? 0.2,
      signal,
    }
  )

  return {
    text: result.content
      .filter((item): item is PiTextContent => item.type === 'text')
      .map((item) => item.text)
      .join(''),
  }
}

function extractTaggedBlockText(rawText: string, tag: 'draft' | 'plan' | 'json', nextTags: string[]): string {
  const openTag = `<${tag}>`
  const closeTag = `</${tag}>`
  const blockStart = rawText.indexOf(openTag)
  if (blockStart < 0) return ''

  const bodyStart = blockStart + openTag.length
  const closeStart = rawText.indexOf(closeTag, bodyStart)
  const nextStarts = nextTags.map((nextTag) => rawText.indexOf(nextTag, bodyStart)).filter((index) => index >= 0)
  const endCandidates = [closeStart, ...nextStarts].filter((index) => index >= 0)
  const bodyEnd =
    endCandidates.length > 0
      ? Math.min(...endCandidates)
      : Math.max(bodyStart, rawText.length - STREAM_MARKER_HOLDBACK_CHARS)
  const text = rawText.slice(bodyStart, bodyEnd)
  return text.replace(/^\s+/, '')
}

async function streamWithPiAi(
  prompt: string,
  options: CreateAnalysisPlannerOptions,
  signal?: AbortSignal
): Promise<PlannerCompletionResult> {
  if (!options.piModel || !options.apiKey) {
    throw new Error('Planner requires piModel and apiKey')
  }

  const stream = streamSimple(
    options.piModel,
    {
      systemPrompt: 'You are a strict planner for ChatLab analysis. Return the requested plan and JSON blocks only.',
      messages: [{ role: 'user', content: [{ type: 'text', text: prompt }], timestamp: Date.now() }] as any,
    },
    {
      apiKey: options.apiKey,
      maxTokens: options.maxTokens ?? 900,
      temperature: options.temperature ?? 0.2,
      signal,
    }
  )

  let text = ''
  let emittedDraftLength = 0
  let emittedPlanLength = 0
  let emittedJsonLength = 0
  let thinkingStartedAt: number | undefined
  let draftThinkingStartedAt: number | undefined
  let draftThinkingEnded = false
  let validationStartedAt: number | undefined
  let validationEnded = false

  for await (const event of stream) {
    if (signal?.aborted) break
    if (event.type === 'thinking_start') {
      thinkingStartedAt = Date.now()
      continue
    }
    if (event.type === 'thinking_delta') {
      options.onThinkingDelta?.(event.delta)
      continue
    }
    if (event.type === 'thinking_end') {
      const durationMs = thinkingStartedAt ? Date.now() - thinkingStartedAt : undefined
      thinkingStartedAt = undefined
      options.onThinkingEnd?.(durationMs)
      continue
    }
    if (event.type !== 'text_delta') continue

    text += event.delta
    const draftText = extractTaggedBlockText(text, 'draft', ['<plan>', '<json>'])
    if (draftText.length > emittedDraftLength) {
      if (!draftThinkingStartedAt) draftThinkingStartedAt = Date.now()
      const delta = draftText.slice(emittedDraftLength)
      emittedDraftLength = draftText.length
      if (delta.trim().length > 0 || /\s/.test(delta)) {
        options.onThinkingDelta?.(delta)
      }
    }
    if (!draftThinkingEnded && emittedDraftLength > 0 && (text.includes('</draft>') || text.includes('<plan>'))) {
      draftThinkingEnded = true
      const durationMs = draftThinkingStartedAt ? Date.now() - draftThinkingStartedAt : undefined
      options.onThinkingEnd?.(durationMs)
    }

    const planText = extractTaggedBlockText(text, 'plan', ['<json>'])
    if (planText.length > emittedPlanLength) {
      const delta = planText.slice(emittedPlanLength)
      emittedPlanLength = planText.length
      if (delta.trim().length > 0 || /\s/.test(delta)) {
        options.onPlanDelta?.(delta)
      }
    }

    const jsonText = extractTaggedBlockText(text, 'json', [])
    if (jsonText.length > emittedJsonLength) {
      if (!validationStartedAt) validationStartedAt = Date.now()
      const delta = jsonText.slice(emittedJsonLength)
      emittedJsonLength = jsonText.length
      if (delta.trim().length > 0 || /\s/.test(delta)) {
        options.onValidationDelta?.(delta)
      }
    }
    if (!validationEnded && emittedJsonLength > 0 && text.includes('</json>')) {
      validationEnded = true
      const durationMs = validationStartedAt ? Date.now() - validationStartedAt : undefined
      options.onValidationEnd?.(durationMs)
    }
  }

  return { text }
}
