/**
 * Member lifecycle — hard-delete a team member end-to-end.
 *
 * A "team member" in this system is:
 *   - one invitations row (status = 'completed')
 *   - one im_channels row (the bot the invitee authorized)
 *   - one workspaces row (provisioner created the invitee workspace)
 *   - one <projectRoot>/users/user-{id}/ directory on disk
 *
 * Deleting a member is irreversible: it drops the im_channel (CASCADE
 * channel_bindings / weixin_*), the workspace (CASCADE sessions / messages /
 * workspace_members), the invitation audit row, and the user directory
 * (shared symlinks + per-user .claude/ snapshots + memory/).
 *
 * Used by:
 *   - DELETE /api/im-channels/[id]   (host removes their own channel)
 *   - DELETE /api/invite/member/[id] (team-view "remove member" button)
 */

import fs from 'fs'
import path from 'path'
import { getDb } from '@/lib/database/client'
import { sql, eq } from 'drizzle-orm'
import { imChannels, workspaces, invitations } from '@/lib/database/schema'
import { getBridgeManager } from '@/lib/im/core/bridge-manager'
import { isUserDirName } from '@/lib/invite/user-id'

export interface DeleteResult {
  /** Whether the channel existed and was owned by userId. */
  deleted: boolean
  /** Reason the delete was a no-op (channel not found / not owned). */
  reason?: 'not_found' | 'not_owned'
  /** Path of the user directory that was (or would have been) removed. */
  userDir?: string
  /** Whether the physical directory existed and was removed. */
  dirRemoved?: boolean
  /** Error from fs.rmSync — non-fatal; the DB delete already succeeded. */
  dirError?: string
}

/**
 * Hard-delete a team member by channel id. The channel must be owned by
 * userId (caller is responsible for fetching the session). Atomicity:
 *
 *   1. Best-effort stopAdapter (IO, before transaction so we don't hold a
 *      write lock on a network round-trip)
 *   2. Transaction: DELETE im_channels → CASCADE inner rows; DELETE
 *      workspaces → CASCADE sessions/messages; DELETE invitations
 *   3. Best-effort fs.rmSync of users/{id}/ (outside the transaction —
 *      filesystem is not part of the DB transaction, and we don't want a
 *      partial write to leave us with a DB-but-no-files or files-but-no-DB
 *      state in the wrong direction)
 *
 * If the DB delete succeeds but the rm fails, the user directory is
 * orphaned on disk; the caller should surface this so a follow-up cleanup
 * pass can sweep it. The directory contains no shared or host-owned data
 * (only symlinks + the invitee's own snapshots + memory/), so it's safe to
 * sweep later.
 */
export async function deleteMemberCompletely(channelId: string, userId: string): Promise<DeleteResult> {
  const db = getDb()

  // One JOIN query: channel + workspace (LEFT) + invitation (LEFT)
  // drizzle 字段 camelCase；下方映射到 row.*（snake_case 别名）保持下游类型兼容
  const joinRow = db
    .select({
      channelId: imChannels.id,
      ownerUserId: imChannels.ownerUserId,
      channelStatus: imChannels.status,
      workspaceId: workspaces.id,
      workspacePath: workspaces.path,
      invitationId: invitations.id,
      inviteeUserId: invitations.createdUserId,
    })
    .from(imChannels)
    .leftJoin(workspaces, eq(workspaces.id, imChannels.defaultWorkspaceId))
    .leftJoin(invitations, eq(invitations.createdChannelId, imChannels.id))
    .where(eq(imChannels.id, channelId))
    .get()

  const row = joinRow
    ? {
        channel_id: joinRow.channelId,
        owner_user_id: joinRow.ownerUserId,
        workspace_path: joinRow.workspacePath,
        workspace_id: joinRow.workspaceId,
        invitation_id: joinRow.invitationId,
        invitee_user_id: joinRow.inviteeUserId,
        channel_status: joinRow.channelStatus,
      }
    : undefined

  if (!row) return { deleted: false, reason: 'not_found' }
  if (row.owner_user_id !== userId) return { deleted: false, reason: 'not_owned' }

  // workspace_path 是 <projectRoot>/users/user-{id}，所以 userDir 等于 workspace_path
  const userDir = row.workspace_path ?? undefined

  // [1] 停止运行中的 adapter（best-effort）
  if (row.channel_status === 'connected') {
    try {
      await getBridgeManager().stopAdapter(channelId)
    } catch (err) {
      console.error(`[lifecycle] stopAdapter failed for ${channelId}:`, err)
    }
  }

  // [2] atomic DB delete (drizzle transaction)
  db.transaction((tx) => {
    tx.delete(imChannels).where(eq(imChannels.id, channelId)).run()
    if (row.workspace_id) {
      tx.delete(workspaces).where(eq(workspaces.id, row.workspace_id)).run()
    }
    if (row.invitation_id) {
      tx.delete(invitations).where(eq(invitations.id, row.invitation_id)).run()
    }
  })

  // [3] filesystem 在事务之外 best-effort 删除
  let dirRemoved: boolean | undefined
  let dirError: string | undefined
  if (userDir) {
    try {
      if (isUserDirName(path.basename(userDir))) {
        fs.rmSync(userDir, { recursive: true, force: true })
        dirRemoved = true
      } else {
        dirError = `Refusing to delete unexpected path: ${userDir}`
      }
    } catch (err) {
      dirError = err instanceof Error ? err.message : String(err)
    }
  }

  return { deleted: true, userDir, dirRemoved, dirError }
}
