import type { FastifyInstance } from 'fastify'
import type { Client } from 'ssh2'
import { z } from 'zod'
import { withSftp } from '../ssh/sftp.js'
import { loadSshHost, connectTarget } from '../ssh/connect.js'

const MAX_EDITABLE_SIZE = 50 * 1024 * 1024 // 50MB - above this, only download is offered

function isBinary(buf: Buffer): boolean {
  const sample = buf.subarray(0, 8000)
  let suspicious = 0
  for (const byte of sample) {
    if (byte === 0) return true
    if (byte < 9 || (byte > 13 && byte < 32)) suspicious++
  }
  return sample.length > 0 && suspicious / sample.length > 0.01
}

function parsePath(req: { query?: unknown; body?: unknown }, key = 'path'): string {
  const fromQuery = (req.query as Record<string, string> | undefined)?.[key]
  const fromBody = (req.body as Record<string, string> | undefined)?.[key]
  const path = fromQuery ?? fromBody
  if (!path) throw new Error('path is required')
  return path
}

export async function fileRoutes(app: FastifyInstance) {
  app.addHook('onRequest', app.authenticate)

  app.get('/api/files/:integrationId/list', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const path = (req.query as { path?: string }).path || '.'
    try {
      const entries = await withSftp(integrationId, (sftp) =>
        new Promise((resolve, reject) => {
          sftp.readdir(path, (err, list) => {
            if (err) reject(err)
            else
              resolve(
                list
                  .map((e) => ({
                    name: e.filename,
                    isDirectory: e.attrs.isDirectory(),
                    isSymlink: e.attrs.isSymbolicLink(),
                    size: e.attrs.size,
                    mode: e.attrs.mode,
                    mtime: e.attrs.mtime,
                  }))
                  .sort((a, b) => (a.isDirectory === b.isDirectory ? a.name.localeCompare(b.name) : a.isDirectory ? -1 : 1)),
              )
          })
        }),
      )
      return { path, entries }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to list directory' })
    }
  })

  app.get('/api/files/:integrationId/content', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    let path: string
    try {
      path = parsePath(req)
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Invalid request' })
    }
    try {
      const result = await withSftp(integrationId, (sftp) =>
        new Promise<{ content: string; encoding: 'utf8' | 'base64'; size: number; mode: number }>((resolve, reject) => {
          sftp.stat(path, (statErr, stats) => {
            if (statErr) {
              reject(statErr)
              return
            }
            if (stats.size > MAX_EDITABLE_SIZE) {
              reject(new Error(`File is too large to view/edit (${Math.round(stats.size / 1024 / 1024)}MB, limit 50MB) - use download instead`))
              return
            }
            sftp.readFile(path, (readErr, buf) => {
              if (readErr) {
                reject(readErr)
                return
              }
              const binary = isBinary(buf)
              resolve({
                content: binary ? buf.toString('base64') : buf.toString('utf8'),
                encoding: binary ? 'base64' : 'utf8',
                size: stats.size,
                mode: stats.mode,
              })
            })
          })
        }),
      )
      return result
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to read file' })
    }
  })

  app.put('/api/files/:integrationId/content', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const bodySchema = z.object({ path: z.string().min(1), content: z.string(), encoding: z.enum(['utf8', 'base64']).default('utf8') })
    const parsed = bodySchema.safeParse(req.body)
    if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    const { path, content, encoding } = parsed.data
    try {
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) => {
          const buf = Buffer.from(content, encoding === 'base64' ? 'base64' : 'utf8')
          sftp.writeFile(path, buf, (err) => (err ? reject(err) : resolve()))
        }),
      )
      return { ok: true }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to write file' })
    }
  })

  app.post('/api/files/:integrationId/mkdir', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const bodySchema = z.object({ path: z.string().min(1) })
    const parsed = bodySchema.safeParse(req.body)
    if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    try {
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) => sftp.mkdir(parsed.data.path, (err) => (err ? reject(err) : resolve()))),
      )
      return { ok: true }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to create directory' })
    }
  })

  app.post('/api/files/:integrationId/rename', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const bodySchema = z.object({ from: z.string().min(1), to: z.string().min(1) })
    const parsed = bodySchema.safeParse(req.body)
    if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    try {
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) => sftp.rename(parsed.data.from, parsed.data.to, (err) => (err ? reject(err) : resolve()))),
      )
      return { ok: true }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to rename' })
    }
  })

  app.post('/api/files/:integrationId/delete', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const bodySchema = z.object({ path: z.string().min(1), isDirectory: z.boolean().default(false) })
    const parsed = bodySchema.safeParse(req.body)
    if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    const { path, isDirectory } = parsed.data
    try {
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) => {
          if (isDirectory) sftp.rmdir(path, (err) => (err ? reject(err) : resolve()))
          else sftp.unlink(path, (err) => (err ? reject(err) : resolve()))
        }),
      )
      return { ok: true }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to delete' })
    }
  })

  app.post('/api/files/:integrationId/chmod', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const bodySchema = z.object({ path: z.string().min(1), mode: z.string().regex(/^[0-7]{3,4}$/) })
    const parsed = bodySchema.safeParse(req.body)
    if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    try {
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) =>
          sftp.chmod(parsed.data.path, parseInt(parsed.data.mode, 8), (err) => (err ? reject(err) : resolve())),
        ),
      )
      return { ok: true }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to chmod' })
    }
  })

  app.get('/api/files/:integrationId/download', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const path = (req.query as { path?: string }).path
    if (!path) return reply.code(400).send({ error: 'path is required' })
    const target = loadSshHost(integrationId)
    if (!target) return reply.code(404).send({ error: 'SSH integration not found' })

    const filename = path.split('/').filter(Boolean).pop() ?? 'download'
    reply.header('content-disposition', `attachment; filename="${filename.replace(/"/g, '')}"`)
    reply.header('content-type', 'application/octet-stream')

    return new Promise((resolve, reject) => {
      let conn: Client | null = null
      let jumpConn: Client | null = null
      let cleaned = false
      const cleanup = () => {
        if (cleaned) return
        cleaned = true
        conn?.end()
        jumpConn?.end()
      }
      const result = connectTarget(
        target,
        (client) => {
          conn = client
          client.sftp((err, sftp) => {
            if (err) {
              cleanup()
              resolve(reply.code(400).send({ error: err.message }))
              return
            }
            const stream = sftp.createReadStream(path)
            stream.on('error', (streamErr: Error) => {
              cleanup()
              reject(streamErr)
            })
            // Tear down the SSH connection only once the HTTP response has
            // actually finished flushing. Resolving the outer promise before
            // that point (e.g. right after calling reply.send) lets Fastify
            // consider the handler done and end the response early, which
            // produced truncated/empty downloads.
            reply.raw.on('finish', cleanup)
            reply.raw.on('close', cleanup)
            resolve(reply.send(stream))
          })
        },
        (message) => {
          cleanup()
          resolve(reply.code(400).send({ error: message }))
        },
      )
      jumpConn = result.jumpConn
    })
  })

  app.post('/api/files/:integrationId/upload', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const data = await req.file()
    if (!data) return reply.code(400).send({ error: 'No file uploaded' })
    const dirPath = (data.fields.path as { value?: string } | undefined)?.value ?? '.'
    const destPath = `${dirPath.replace(/\/$/, '')}/${data.filename}`
    try {
      const buf = await data.toBuffer()
      await withSftp(integrationId, (sftp) =>
        new Promise<void>((resolve, reject) => sftp.writeFile(destPath, buf, (err) => (err ? reject(err) : resolve()))),
      )
      return { ok: true, path: destPath }
    } catch (err) {
      return reply.code(400).send({ error: err instanceof Error ? err.message : 'Failed to upload file' })
    }
  })
}
