import { randomUUID } from 'node:crypto'
import type { Client, SFTPWrapper, FileEntry } from 'ssh2'
import { loadSshHost, connectTarget } from './connect.js'

/**
 * Host-to-host file transfer, streamed through the backend: read from the source
 * host's SFTP and write to the destination host's SFTP, item by item.
 *
 * This deliberately ports only the core of Termix's host-transfer feature (its
 * "item_sftp" path). The fork's parallel-segment workers, tar-vs-sftp heuristics,
 * hung-stream watchdogs and retry orchestration (~3,400 lines) are left behind:
 * at this app's scale a single streamed copy per file is simple, correct, and
 * cancellable, which is what the feature actually needs.
 */

export type TransferStatus = 'running' | 'completed' | 'failed' | 'cancelled'

export interface TransferProgress {
  transferId: string
  status: TransferStatus
  sourceIntegrationId: number
  destIntegrationId: number
  sourcePaths: string[]
  destPath: string
  move: boolean
  totalFiles: number
  totalBytes: number
  filesTransferred: number
  bytesTransferred: number
  currentFile: string | null
  error: string | null
  startedAt: number
  finishedAt: number | null
}

interface SftpConnection {
  conn: Client
  jumpConn: Client | null
  sftp: SFTPWrapper
}

const activeTransfers = new Map<string, TransferProgress>()
const cancelRequested = new Set<string>()

function openSftp(integrationId: number): Promise<SftpConnection> {
  const target = loadSshHost(integrationId)
  if (!target) return Promise.reject(new Error(`SSH integration ${integrationId} not found`))
  return new Promise<SftpConnection>((resolve, reject) => {
    let jumpConn: Client | null = null
    const result = connectTarget(
      target,
      (client) => {
        client.sftp((err, sftp) => {
          if (err) {
            client.end()
            jumpConn?.end()
            reject(err)
            return
          }
          resolve({ conn: client, jumpConn, sftp })
        })
      },
      (message) => {
        jumpConn?.end()
        reject(new Error(message))
      },
    )
    jumpConn = result.jumpConn
  })
}

function closeSftp(c: SftpConnection | null) {
  if (!c) return
  c.conn.end()
  c.jumpConn?.end()
}

function sftpStat(sftp: SFTPWrapper, path: string) {
  return new Promise<import('ssh2').Stats>((resolve, reject) =>
    sftp.stat(path, (err, stats) => (err ? reject(err) : resolve(stats))),
  )
}

function sftpReaddir(sftp: SFTPWrapper, path: string) {
  return new Promise<FileEntry[]>((resolve, reject) =>
    sftp.readdir(path, (err, list) => (err ? reject(err) : resolve(list))),
  )
}

function sftpMkdir(sftp: SFTPWrapper, path: string) {
  return new Promise<void>((resolve) =>
    // tolerate "already exists" — the only failure mode we care about surfaces on write
    sftp.mkdir(path, () => resolve()),
  )
}

function sftpUnlink(sftp: SFTPWrapper, path: string) {
  return new Promise<void>((resolve, reject) =>
    sftp.unlink(path, (err) => (err ? reject(err) : resolve())),
  )
}

function sftpRmdir(sftp: SFTPWrapper, path: string) {
  return new Promise<void>((resolve, reject) =>
    sftp.rmdir(path, (err) => (err ? reject(err) : resolve())),
  )
}

interface WalkItem {
  sourcePath: string
  destPath: string
  isDirectory: boolean
  size: number
}

/** Depth-first walk of a source item, producing the list of dirs+files to create/copy. */
async function walk(sftp: SFTPWrapper, sourcePath: string, destPath: string): Promise<WalkItem[]> {
  const stats = await sftpStat(sftp, sourcePath)
  if (stats.isDirectory()) {
    const items: WalkItem[] = [{ sourcePath, destPath, isDirectory: true, size: 0 }]
    const entries = await sftpReaddir(sftp, sourcePath)
    for (const entry of entries) {
      const childSource = `${sourcePath.replace(/\/$/, '')}/${entry.filename}`
      const childDest = `${destPath.replace(/\/$/, '')}/${entry.filename}`
      items.push(...(await walk(sftp, childSource, childDest)))
    }
    return items
  }
  return [{ sourcePath, destPath, isDirectory: false, size: stats.size }]
}

function streamCopy(
  source: SftpConnection,
  dest: SftpConnection,
  item: WalkItem,
  transferId: string,
  onChunk: (bytes: number) => void,
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    const readStream = source.sftp.createReadStream(item.sourcePath)
    const writeStream = dest.sftp.createWriteStream(item.destPath)
    let settled = false
    const finish = (err?: Error) => {
      if (settled) return
      settled = true
      if (err) {
        readStream.destroy()
        writeStream.destroy()
        reject(err)
      } else {
        resolve()
      }
    }
    readStream.on('data', (chunk: Buffer) => {
      if (cancelRequested.has(transferId)) {
        finish(new Error('Transfer cancelled'))
        return
      }
      onChunk(chunk.length)
    })
    readStream.on('error', finish)
    writeStream.on('error', finish)
    writeStream.on('close', () => finish())
    readStream.pipe(writeStream)
  })
}

async function run(progress: TransferProgress) {
  let source: SftpConnection | null = null
  let dest: SftpConnection | null = null
  try {
    source = await openSftp(progress.sourceIntegrationId)
    dest = await openSftp(progress.destIntegrationId)

    // Scan phase: enumerate everything and compute totals up front so the UI can show a real bar.
    const allItems: WalkItem[] = []
    for (const sourcePath of progress.sourcePaths) {
      const name = sourcePath.replace(/\/$/, '').split('/').filter(Boolean).pop() ?? sourcePath
      const itemDest = `${progress.destPath.replace(/\/$/, '')}/${name}`
      allItems.push(...(await walk(source.sftp, sourcePath, itemDest)))
    }
    const files = allItems.filter((i) => !i.isDirectory)
    progress.totalFiles = files.length
    progress.totalBytes = files.reduce((sum, f) => sum + f.size, 0)

    // Create the destination root + all directories first (depth-first order keeps parents before children).
    await sftpMkdir(dest.sftp, progress.destPath)
    for (const dir of allItems.filter((i) => i.isDirectory)) {
      await sftpMkdir(dest.sftp, dir.destPath)
    }

    // Copy files.
    for (const file of files) {
      if (cancelRequested.has(progress.transferId)) throw new Error('Transfer cancelled')
      progress.currentFile = file.sourcePath
      await streamCopy(source, dest, file, progress.transferId, (bytes) => {
        progress.bytesTransferred += bytes
      })
      progress.filesTransferred += 1
    }

    // On move, remove the source tree (files first, then dirs deepest-first).
    if (progress.move) {
      for (const file of files) await sftpUnlink(source.sftp, file.sourcePath)
      const dirs = allItems.filter((i) => i.isDirectory).reverse()
      for (const dir of dirs) await sftpRmdir(source.sftp, dir.sourcePath)
    }

    progress.status = 'completed'
    progress.currentFile = null
  } catch (err) {
    progress.status = cancelRequested.has(progress.transferId) ? 'cancelled' : 'failed'
    progress.error = err instanceof Error ? err.message : 'Transfer failed'
  } finally {
    progress.finishedAt = Date.now()
    cancelRequested.delete(progress.transferId)
    closeSftp(source)
    closeSftp(dest)
  }
}

export function startTransfer(req: {
  sourceIntegrationId: number
  destIntegrationId: number
  sourcePaths: string[]
  destPath: string
  move?: boolean
}): string {
  const transferId = randomUUID()
  const progress: TransferProgress = {
    transferId,
    status: 'running',
    sourceIntegrationId: req.sourceIntegrationId,
    destIntegrationId: req.destIntegrationId,
    sourcePaths: req.sourcePaths,
    destPath: req.destPath,
    move: req.move ?? false,
    totalFiles: 0,
    totalBytes: 0,
    filesTransferred: 0,
    bytesTransferred: 0,
    currentFile: null,
    error: null,
    startedAt: Date.now(),
    finishedAt: null,
  }
  activeTransfers.set(transferId, progress)
  void run(progress)
  return transferId
}

export function getTransfer(transferId: string): TransferProgress | undefined {
  return activeTransfers.get(transferId)
}

export function listTransfers(): TransferProgress[] {
  return Array.from(activeTransfers.values()).sort((a, b) => b.startedAt - a.startedAt)
}

export function cancelTransfer(transferId: string): boolean {
  const progress = activeTransfers.get(transferId)
  if (!progress || progress.status !== 'running') return false
  cancelRequested.add(transferId)
  return true
}

/** Drops finished transfers older than maxAgeMs so the map doesn't grow unbounded. */
export function cleanupOldTransfers(maxAgeMs = 60 * 60 * 1000): void {
  const now = Date.now()
  for (const [id, progress] of activeTransfers.entries()) {
    if (progress.status !== 'running' && progress.finishedAt && now - progress.finishedAt > maxAgeMs) {
      activeTransfers.delete(id)
    }
  }
}
