// Profile-delete routing logic for the `hermes:api` IPC handler.
//
// When the renderer issues DELETE /api/profiles/<name>, the handler must
// tear down every local backend for that profile and route the DELETE itself
// away from the just-deleted profile. Concurrent and delayed starts must also
// be rejected: spawning a fresh backend would call ensure_hermes_home() and
// recreate the profile directory the delete just removed, leaving a zombie
// process behind (issue #52279).
//
// These helpers are pure so they can be unit-tested without Electron.

/** Parse a profile name from an `/api/profiles/<name>` request path. */
export function profileNameFromPath(path: unknown): string | null {
  const match = String(path || '').match(/^\/api\/profiles\/([^/?#]+)(?:[?#].*)?$/)

  if (!match) {
    return null
  }

  let raw = ''

  try {
    raw = decodeURIComponent(match[1])
  } catch {
    return null
  }

  const name = raw.trim()

  if (!name) {
    return null
  }

  if (name.toLowerCase() === 'default') {
    return 'default'
  }

  return name.toLowerCase()
}

/** Parse a `hermes:api` request into the profile name a DELETE targets. */
export function profileNameFromDeleteRequest(request) {
  if (!request || String(request.method || 'GET').toUpperCase() !== 'DELETE') {
    return null
  }

  return profileNameFromPath(request.path)
}

export type ProfileDeleteAction = 'noop' | 'teardown-primary' | 'teardown-pool'

export interface ProfileDeleteDecision {
  action: ProfileDeleteAction
  profile: string | null
}

export interface ProfileDeleteDecisionDeps {
  isDefaultProfile: (profile: string) => boolean
  isValidProfileName: (profile: string) => boolean
  primaryProfileKey: () => string
}

export interface ConnectionScopedProfileDeleteRequest {
  connectionId?: unknown
  method?: unknown
  path?: unknown
  profile?: unknown
}

export interface ConnectionScopedProfileDeleteDeps<T> {
  acquire: (profile: string) => () => void
  connectionKind: (connectionId: string) => string
  dispatch: (routeProfile: null) => Promise<T>
  isDefaultProfile: (profile: string) => boolean
  isValidProfileName: (profile: string) => boolean
  prepareLocal: (request: ConnectionScopedProfileDeleteRequest) => Promise<void>
  teardownConnection: (connectionId: string, profile: string) => Promise<void>
}

/**
 * Run an explicit registry profile DELETE under the same process-wide gate as
 * legacy deletion. Teardown and dispatch are injected so the Electron caller
 * can stop either local profile pools or one connection-qualified backend.
 * Dispatch deliberately receives a null route profile: resolving the deleted
 * profile again would recurse into the gate (and could recreate its home).
 */
export async function dispatchConnectionScopedProfileDelete<T>(
  request: ConnectionScopedProfileDeleteRequest,
  deps: ConnectionScopedProfileDeleteDeps<T>
): Promise<T> {
  const targetProfile = profileNameFromDeleteRequest(request)
  const logicalProfile = String(request.profile ?? '').trim() || targetProfile || ''
  const connectionId = String(request.connectionId ?? '').trim()

  if (!targetProfile || !connectionId) {
    throw new Error('Connection-scoped profile deletion requires a connection and profile.')
  }

  if (deps.isDefaultProfile(targetProfile)) {
    throw new Error('The default profile cannot be deleted.')
  }

  if (!deps.isValidProfileName(targetProfile)) {
    throw new Error(`Invalid profile name: ${targetProfile}`)
  }

  const release = deps.acquire(targetProfile)

  try {
    if (deps.connectionKind(connectionId) === 'local') {
      await deps.prepareLocal(request)
    } else {
      await deps.teardownConnection(connectionId, logicalProfile)
    }

    return await deps.dispatch(null)
  } finally {
    release()
  }
}

/**
 * Process-local barrier for profile deletion. Electron IPC handlers run
 * concurrently, so tearing down a pooled backend is not enough by itself: a
 * renderer reconnect can enter ensureBackend() while the DELETE request is
 * still removing the profile and recreate its HERMES_HOME.
 *
 * Counts instead of a Set keep overlapping requests for the same profile
 * blocked until the last request releases its lease.
 */
export class ProfileDeletionGate {
  readonly #active = new Map<string, number>()

  acquire(profile: unknown): () => void {
    const key = String(profile ?? '')
      .trim()
      .toLowerCase()

    if (!key) {
      return () => undefined
    }

    this.#active.set(key, (this.#active.get(key) ?? 0) + 1)
    let released = false

    return () => {
      if (released) {
        return
      }

      released = true
      const remaining = (this.#active.get(key) ?? 1) - 1

      if (remaining > 0) {
        this.#active.set(key, remaining)
      } else {
        this.#active.delete(key)
      }
    }
  }

  blocks(profile: unknown): boolean {
    const key = String(profile ?? '')
      .trim()
      .toLowerCase()

    return Boolean(key && this.#active.has(key))
  }

  assertCanStart(profile: unknown): void {
    const key = String(profile ?? '').trim()

    if (this.blocks(key)) {
      throw new Error(`Profile "${key}" is being deleted.`)
    }
  }
}

/**
 * Validate the final boundary before spawning a local profile backend. The
 * deletion gate closes the in-flight race; the directory check rejects a
 * delayed renderer retry after the DELETE request has already completed.
 */
export function assertLocalProfileCanStart(
  profile: unknown,
  gate: ProfileDeletionGate,
  profileDirectoryExists: (profile: string) => boolean
): void {
  const key = String(profile ?? '')
    .trim()
    .toLowerCase()

  gate.assertCanStart(key)

  if (key && key !== 'default' && !profileDirectoryExists(key)) {
    throw new Error(`Profile "${key}" no longer exists.`)
  }
}

/**
 * A local profile can occupy the legacy bare pool slot or the explicit-local
 * registry slot when the v1 route points elsewhere. Both processes own the
 * same on-disk profile and must be stopped before deleting it.
 */
export function localProfilePoolKeys(profile: unknown): string[] {
  const key = String(profile ?? '')
    .trim()
    .toLowerCase()

  return key ? [key, `conn:local::${key}`] : []
}

/**
 * Pure decision logic for prepareProfileDeleteRequest: given the parsed
 * profile name (or null), decide which side-effecting branch the caller
 * should take and what profile name it should ultimately report as
 * torn-down. No I/O, no async -- the caller performs the actual teardown
 * based on `action`.
 */
export function decideProfileDeleteAction(
  profile: string | null,
  deps: ProfileDeleteDecisionDeps
): ProfileDeleteDecision {
  if (!profile || deps.isDefaultProfile(profile) || !deps.isValidProfileName(profile)) {
    return { action: 'noop', profile: null }
  }

  if (profile === deps.primaryProfileKey()) {
    return { action: 'teardown-primary', profile }
  }

  return { action: 'teardown-pool', profile }
}

/**
 * Route the next `hermes:api` request away from the primary/window backend
 * whenever a profile was just torn down -- otherwise ensureBackend would
 * spawn a fresh pool backend for the deleted profile, whose
 * ensure_hermes_home() recreates the directory the delete just removed.
 */
export function resolveRouteProfile(
  tornDownProfile: string | null,
  profile: string | null | undefined
): string | null | undefined {
  return tornDownProfile ? null : profile
}
