import 'server-only'
import { spawn } from 'node:child_process'
import { db } from '@/lib/db'

export interface AIReviewSessionRow {
  id: string
  status: 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED'
  queueDepth: number
  startedAt: Date
  runningAt: Date | null
  finishedAt: Date | null
  errorMessage: string | null
  logTail: string | null
}

// Items the home-server identify pass will pick up: untitled fresh captures,
// plus already-named items the user has flagged for re-review (added a voice
// note or pendingNote since the last pass).
export async function getQueueDepth(): Promise<number> {
  return db.item.count({
    where: { OR: [{ name: 'Untitled item' }, { needsAIReview: true }] },
  })
}

export async function getLatestSession(): Promise<AIReviewSessionRow | null> {
  const row = await db.aIReviewSession.findFirst({ orderBy: { startedAt: 'desc' } })
  return row as AIReviewSessionRow | null
}

export async function getSession(id: string): Promise<AIReviewSessionRow | null> {
  const row = await db.aIReviewSession.findUnique({ where: { id } })
  return row as AIReviewSessionRow | null
}

interface StartResult {
  sessionId: string
  queueDepth: number
}

export async function startSession(opts: { triggeredById?: string }): Promise<StartResult> {
  const queueDepth = await getQueueDepth()
  if (queueDepth === 0) {
    throw new Error('Queue is empty — nothing to review.')
  }

  // Reject if another session is already in-flight; we don't want two passes
  // racing the same items.
  const inflight = await db.aIReviewSession.findFirst({
    where: { status: { in: ['PENDING', 'RUNNING'] } },
  })
  if (inflight) {
    throw new Error(`A session is already ${inflight.status.toLowerCase()} (started ${inflight.startedAt.toISOString()}).`)
  }

  const session = await db.aIReviewSession.create({
    data: { queueDepth, triggeredById: opts.triggeredById ?? null },
  })

  try {
    await launchHomeServerScript(session.id)
  } catch (err) {
    await db.aIReviewSession.update({
      where: { id: session.id },
      data: {
        status: 'FAILED',
        finishedAt: new Date(),
        errorMessage: err instanceof Error ? err.message.slice(0, 500) : 'launch failed',
      },
    })
    throw err
  }

  return { sessionId: session.id, queueDepth }
}

// Once the pending/created queue reaches this depth, the next item to enter
// it auto-launches an identify pass — so a field capture session turns into
// named items without anyone tapping "Start AI pass". Set
// IDENTIFY_AUTO_THRESHOLD=0 to disable auto-triggering entirely.
function autoStartThreshold(): number {
  const raw = process.env.IDENTIFY_AUTO_THRESHOLD
  if (raw === undefined || raw === '') return 7
  const n = Number(raw)
  return Number.isFinite(n) ? n : 7
}

// Fire-and-forget hook: call after an item enters the pending-AI state
// (fresh capture, pendingNote, or new voice note). Starts a pass only when
// the queue has reached the threshold and nothing is already in-flight.
//
// Never throws. Auto-triggering is a convenience that hangs off the
// capture/note flows, so it must not break them — the common "a session is
// already running" / "queue is empty" cases from startSession are swallowed,
// and a genuine ssh launch failure is already recorded on the failed session
// row by startSession, so there's nothing useful to surface to the caller.
export async function maybeAutoStartSession(): Promise<void> {
  const threshold = autoStartThreshold()
  if (threshold <= 0) return
  try {
    const depth = await getQueueDepth()
    if (depth < threshold) return
    await startSession({ triggeredById: 'auto' })
  } catch {
    // expected when a pass is already in-flight or the queue emptied; ignore
  }
}

// SSHes to the home server and runs the identify wrapper script in the
// background. We expect ssh to return quickly because the wrapper redirects
// its own stdio and detaches; if ssh hangs longer than HOME_SSH_TIMEOUT_MS we
// abort and treat the launch as failed.
async function launchHomeServerScript(sessionId: string): Promise<void> {
  const cfg = readSshConfig()

  // We send just the session UUID. The home-server authorized_keys entry has
  // a forced `command="…run-identify-local.sh"` so the script always runs
  // regardless of what we ship; it reads the UUID from $SSH_ORIGINAL_COMMAND.
  const args = [
    '-i', cfg.keyPath,
    '-p', String(cfg.port),
    '-o', 'BatchMode=yes',
    '-o', 'ConnectTimeout=10',
    '-o', 'StrictHostKeyChecking=accept-new',
    '-o', `UserKnownHostsFile=${cfg.knownHostsPath}`,
    `${cfg.user}@${cfg.host}`,
    sessionId,
  ]

  await new Promise<void>((resolve, reject) => {
    const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] })
    let stderr = ''
    const timer = setTimeout(() => {
      child.kill('SIGTERM')
      reject(new Error('ssh timed out launching home-server script'))
    }, 15_000)

    child.stderr.on('data', chunk => {
      stderr += chunk.toString()
      if (stderr.length > 2000) stderr = stderr.slice(-2000)
    })
    child.on('error', err => {
      clearTimeout(timer)
      reject(new Error(`ssh spawn failed: ${err.message}`))
    })
    child.on('exit', code => {
      clearTimeout(timer)
      if (code === 0) resolve()
      else reject(new Error(`ssh exited ${code}: ${stderr.trim() || '(no stderr)'}`))
    })
  })
}

interface SshConfig {
  user: string
  host: string
  port: number
  keyPath: string
  knownHostsPath: string
}

function readSshConfig(): SshConfig {
  return {
    user: process.env.IDENTIFY_HOME_SSH_USER || 'erictran',
    host: process.env.IDENTIFY_HOME_SSH_HOST || '67.182.44.118',
    port: Number(process.env.IDENTIFY_HOME_SSH_PORT || 2222),
    keyPath: process.env.IDENTIFY_HOME_SSH_KEY || '/etc/eorganize/identify_to_home',
    knownHostsPath: process.env.IDENTIFY_HOME_KNOWN_HOSTS || '/etc/eorganize/known_hosts',
  }
}
