import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import { resolveDimcodeUserDir } from '#cli/core/storage-paths'

export const ACTIVE_SESSION_ENV_KEYS = {
  sessionId: ['DIMCODE_ACTIVE_SESSION_ID'] as const,
  folder: ['DIMCODE_ACTIVE_SESSION_FOLDER'] as const,
}

export type ActiveSessionVisibility
  = | 'focused'
    | 'blurred'
    | 'suspended'
    | 'inactive'

type ActiveSessionRecord = Readonly<{
  version: 1
  app: 'dim'
  pid: number
  sessionId: string | null
  cwd: string | null
  title: string | null
  mode: string | null
  providerId: string | null
  providerName: string | null
  modelId: string | null
  focused: boolean
  visibility: ActiveSessionVisibility
  source: string
  updatedAt: string
}>

type ActiveSessionPaths = Readonly<{
  runtimeDir: string
  activePath: string
  legacyInstancesDir: string
}>

let currentInstanceRecord: ActiveSessionRecord | null = null
let runtimeMutationTail: Promise<void> = Promise.resolve()

function normalizeText(value: unknown): string | null {
  const text = String(value ?? '').trim()
  return text || null
}

function resolveUserDir(env: Record<string, string | undefined>): string {
  return resolveDimcodeUserDir(env)
}

function resolvePaths(
  env: Record<string, string | undefined> = process.env,
): ActiveSessionPaths {
  const runtimeDir = path.resolve(resolveUserDir(env), 'runtime')
  return {
    runtimeDir,
    activePath: path.resolve(runtimeDir, 'active-session.json'),
    legacyInstancesDir: path.resolve(runtimeDir, 'instances'),
  }
}

function enqueueRuntimeMutation(task: () => Promise<void>): void {
  runtimeMutationTail = runtimeMutationTail
    .catch(() => {})
    .then(task)
    .catch(() => {})
}

async function writeJsonAtomic(
  filePath: string,
  value: unknown,
): Promise<void> {
  const dir = path.dirname(filePath)
  await fsp.mkdir(dir, { recursive: true })
  const tmpPath = path.resolve(
    dir,
    `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
  )
  await fsp.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
  await fsp.rename(tmpPath, filePath)
}

function readJsonFileSync<T>(filePath: string): T | null {
  try {
    const raw = fs.readFileSync(filePath, 'utf8')
    const parsed = JSON.parse(raw)
    return parsed as T
  }
  catch {
    return null
  }
}

async function removeDirIfExists(dirPath: string): Promise<void> {
  try {
    await fsp.rm(dirPath, { recursive: true, force: true })
  }
  catch {
    // ignore
  }
}

function isProcessAlive(pid: number): boolean {
  if (!Number.isFinite(pid) || pid <= 0)
    return false
  try {
    process.kill(pid, 0)
    return true
  }
  catch (error: unknown) {
    if (
      error != null
      && typeof error === 'object'
      && 'code' in error
      && String(error.code ?? '') === 'EPERM'
    ) {
      return true
    }
    return false
  }
}

function writeEnvValue(key: string, value: string | undefined): void {
  if (value)
    process.env[key] = value
  else delete process.env[key]
}

function applyProcessEnv(
  opts?: Readonly<{ sessionId?: string | null, cwd?: string | null }>,
): void {
  const sessionId = normalizeText(opts?.sessionId)
  const cwd = normalizeText(opts?.cwd)
  for (const key of ACTIVE_SESSION_ENV_KEYS.sessionId)
    writeEnvValue(key, sessionId ?? undefined)
  for (const key of ACTIVE_SESSION_ENV_KEYS.folder)
    writeEnvValue(key, cwd ?? undefined)
}

function createRecord(
  opts: Readonly<{
    sessionId?: string | null
    cwd?: string | null
    title?: string | null
    mode?: string | null
    providerId?: string | null
    providerName?: string | null
    modelId?: string | null
    focused: boolean
    visibility: ActiveSessionVisibility
    source: string
    pid?: number
  }>,
): ActiveSessionRecord {
  return {
    version: 1,
    app: 'dim',
    pid: Number.isFinite(opts.pid) ? Math.trunc(opts.pid!) : process.pid,
    sessionId: normalizeText(opts.sessionId),
    cwd: normalizeText(opts.cwd),
    title: normalizeText(opts.title),
    mode: normalizeText(opts.mode),
    providerId: normalizeText(opts.providerId),
    providerName: normalizeText(opts.providerName),
    modelId: normalizeText(opts.modelId),
    focused: Boolean(opts.focused),
    visibility: opts.visibility,
    source: normalizeText(opts.source) ?? 'unknown',
    updatedAt: new Date().toISOString(),
  }
}

function areRecordsEquivalent(
  left: ActiveSessionRecord | null,
  right: ActiveSessionRecord | null,
): boolean {
  if (!left || !right)
    return false
  return (
    left.pid === right.pid
    && left.sessionId === right.sessionId
    && left.cwd === right.cwd
    && left.title === right.title
    && left.mode === right.mode
    && left.providerId === right.providerId
    && left.providerName === right.providerName
    && left.modelId === right.modelId
    && left.focused === right.focused
    && left.visibility === right.visibility
  )
}

function applyProcessEnvForRecord(record: ActiveSessionRecord): void {
  if (record.focused && record.sessionId && record.cwd) {
    applyProcessEnv({ sessionId: record.sessionId, cwd: record.cwd })
    return
  }
  applyProcessEnv({ sessionId: null, cwd: null })
}

function canReplaceActiveRecord(
  activePath: string,
  pid: number = process.pid,
): boolean {
  const existing = readJsonFileSync<Partial<ActiveSessionRecord>>(activePath)
  const ownerPid = Number(existing?.pid ?? 0)
  return !existing || ownerPid === pid || !isProcessAlive(ownerPid)
}

async function writeActiveRecordIfOwned(
  activePath: string,
  record: ActiveSessionRecord,
): Promise<void> {
  if (!canReplaceActiveRecord(activePath, record.pid))
    return
  await writeJsonAtomic(activePath, record)
}

async function cleanupLegacyRuntime(paths: ActiveSessionPaths): Promise<void> {
  await removeDirIfExists(paths.legacyInstancesDir)
}

export function publishActiveSession(
  opts: Readonly<{
    sessionId?: string | null
    cwd?: string | null
    title?: string | null
    mode?: string | null
    providerId?: string | null
    providerName?: string | null
    modelId?: string | null
    source?: string
    env?: Record<string, string | undefined>
  }>,
): void {
  const sessionId = normalizeText(opts.sessionId)
  const cwd = normalizeText(opts.cwd)
  if (!sessionId || !cwd) {
    clearActiveSession({
      source: opts.source ?? 'publish_invalid',
      env: opts.env,
    })
    return
  }

  const record = createRecord({
    sessionId,
    cwd,
    title: opts.title,
    mode: opts.mode,
    providerId: opts.providerId,
    providerName: opts.providerName,
    modelId: opts.modelId,
    focused: true,
    visibility: 'focused',
    source: opts.source ?? 'publish',
  })
  const paths = resolvePaths(opts.env)
  if (areRecordsEquivalent(currentInstanceRecord, record)) {
    applyProcessEnvForRecord(record)
    return
  }
  currentInstanceRecord = record
  enqueueRuntimeMutation(async () => {
    await cleanupLegacyRuntime(paths)
    await writeJsonAtomic(paths.activePath, record)
  })
  applyProcessEnvForRecord(record)
}

export function clearActiveSession(
  opts?: Readonly<{
    source?: string
    env?: Record<string, string | undefined>
  }>,
): void {
  const paths = resolvePaths(opts?.env)
  const record = createRecord({
    sessionId: null,
    cwd: null,
    title: null,
    mode: null,
    providerId: null,
    providerName: null,
    modelId: null,
    focused: false,
    visibility: 'inactive',
    source: opts?.source ?? 'clear',
  })
  if (areRecordsEquivalent(currentInstanceRecord, record)) {
    applyProcessEnvForRecord(record)
    return
  }
  currentInstanceRecord = record
  enqueueRuntimeMutation(async () => {
    await cleanupLegacyRuntime(paths)
    await writeActiveRecordIfOwned(paths.activePath, record)
  })
  applyProcessEnvForRecord(record)
}

export function updateActiveSessionVisibility(
  opts: Readonly<{
    visibility: Extract<
      ActiveSessionVisibility,
      'focused' | 'blurred' | 'suspended'
    >
    source?: string
    env?: Record<string, string | undefined>
  }>,
): void {
  const paths = resolvePaths(opts.env)
  const existing = currentInstanceRecord
  if (!existing)
    return

  const next = createRecord({
    sessionId: existing.sessionId,
    cwd: existing.cwd,
    title: existing.title,
    mode: existing.mode,
    providerId: existing.providerId,
    providerName: existing.providerName,
    modelId: existing.modelId,
    focused: opts.visibility === 'focused',
    visibility: opts.visibility,
    source: opts.source ?? opts.visibility,
    pid: existing.pid,
  })
  if (areRecordsEquivalent(existing, next)) {
    applyProcessEnvForRecord(next)
    return
  }
  currentInstanceRecord = next

  if (next.sessionId && next.cwd && next.focused) {
    enqueueRuntimeMutation(async () => {
      await cleanupLegacyRuntime(paths)
      await writeJsonAtomic(paths.activePath, next)
    })
    applyProcessEnvForRecord(next)
    return
  }

  enqueueRuntimeMutation(async () => {
    await cleanupLegacyRuntime(paths)
    await writeActiveRecordIfOwned(paths.activePath, next)
  })
  applyProcessEnvForRecord(next)
}

export function teardownActiveSession(
  opts?: Readonly<{
    source?: string
    env?: Record<string, string | undefined>
  }>,
): void {
  const paths = resolvePaths(opts?.env)
  const pid = Number(currentInstanceRecord?.pid ?? process.pid)
  const record = createRecord({
    sessionId: null,
    cwd: null,
    title: null,
    mode: null,
    providerId: null,
    providerName: null,
    modelId: null,
    focused: false,
    visibility: 'inactive',
    source: opts?.source ?? 'teardown',
    pid,
  })
  currentInstanceRecord = null
  enqueueRuntimeMutation(async () => {
    await cleanupLegacyRuntime(paths)
    await writeActiveRecordIfOwned(paths.activePath, record)
  })
  applyProcessEnvForRecord(record)
}

export function readActiveSessionRecordForTests(
  env?: Record<string, string | undefined>,
): ActiveSessionRecord | null {
  const paths = resolvePaths(env)
  return readJsonFileSync<ActiveSessionRecord>(paths.activePath)
}

export async function flushActiveSessionWritesForTests(): Promise<void> {
  await runtimeMutationTail
}
