// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import { mkdirSync, readFileSync, statSync } from 'node:fs'
import { dirname, resolve, sep } from 'node:path'
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import type { Limit } from '@struktoai/mirage-core/types'
import type { MountSpec } from '@struktoai/mirage-core/workspace/workspace/workspace'
import type { Resource } from '@struktoai/mirage-core/resource/base'
import { DiskWorkspaceStateStore, Workspace } from '@struktoai/mirage-node'
import { newWorkspaceId } from '@struktoai/mirage-core/utils/ids'
import { type WorkspaceRegistry } from '../registry.ts'
import { buildOverrideResources, cloneWorkspaceWithOverride, type OverrideShape } from '../clone.ts'
import {
  configToWorkspaceArgs,
  loadWorkspaceConfig,
  type WorkspaceArgs,
  type WorkspaceConfigRaw,
} from '@struktoai/mirage-node'
import { makeBrief, makeDetail } from '../summary.ts'

export interface WorkspaceRoutesDeps {
  registry: WorkspaceRegistry
  snapshotRoot: string
  stateRoot: string
}

const WRITE_RATE_LIMIT = {
  config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
}

interface CreateWorkspaceBody {
  config: Record<string, unknown>
  id?: string
}

interface WorkspaceIdParams {
  id: string
}

interface WorkspaceGetQuery {
  verbose?: string
}

interface CloneWorkspaceBody {
  id?: string
  override?: OverrideShape
}

interface SnapshotWorkspaceBody {
  path: string
}

interface LoadWorkspaceBody {
  path: string
  id?: string
  override?: OverrideShape
}

export function registerWorkspacesRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps): void {
  app.post<{ Body: CreateWorkspaceBody }>(
    '/v1/workspaces',
    async (req: FastifyRequest<{ Body: CreateWorkspaceBody }>, reply: FastifyReply) => {
      const body = req.body
      const config: unknown = body.config
      if (config === null || typeof config !== 'object' || Array.isArray(config)) {
        return reply.status(400).send({ detail: 'config must be a mapping' })
      }
      if (body.id !== undefined && deps.registry.has(body.id)) {
        return reply.status(409).send({ detail: `workspace id already exists: ${body.id}` })
      }
      let cfg: WorkspaceConfigRaw
      try {
        cfg = loadWorkspaceConfig(config as Record<string, unknown>)
      } catch (e) {
        return reply.status(400).send({ detail: (e as Error).message })
      }
      let args: WorkspaceArgs
      try {
        args = await configToWorkspaceArgs(cfg)
      } catch (e) {
        return reply.status(502).send({ detail: `resource build failed: ${(e as Error).message}` })
      }
      const resourceMap: Record<string, MountSpec> = {}
      const commandLimits: Record<string, Record<string, Limit>> = {}
      for (const [prefix, [resource, mode, limits]] of Object.entries(args.resources)) {
        resourceMap[prefix] = [resource, mode]
        if (Object.keys(limits).length > 0) commandLimits[prefix] = limits
      }
      // The registry id and the state-store scope must be the same identity,
      // so resolve it before construction: explicit REST id, then the
      // config's workspaceId, then a fresh mint.
      const wid = body.id ?? args.options.workspaceId ?? newWorkspaceId()
      let ws: Workspace
      try {
        // Every option the config produced rides through: enumerating
        // them by hand silently dropped `clis` and `guards`, so a yaml
        // clis block parsed, validated, and then installed nothing.
        // Only identity and the store default are the daemon's to
        // decide.
        ws = new Workspace(resourceMap, {
          ...args.options,
          workspaceId: wid,
          // Daemon default is disk (a created workspace survives restart
          // with zero infrastructure, like git init); the library default
          // stays ram. An explicit store always wins.
          store: args.options.store ?? new DiskWorkspaceStateStore({ root: deps.stateRoot }),
          // Whichever of the two built it, no sibling workspace shares
          // it, so this workspace is the one that closes it.
          ownsStore: true,
          ...(Object.keys(commandLimits).length > 0 ? { commandLimits } : {}),
        })
      } catch (e) {
        return reply.status(400).send({ detail: (e as Error).message })
      }
      let entry
      try {
        for (const [prefix, [backend, mountpoint]] of Object.entries(args.kernelMounts)) {
          await ws.addFuseMount(prefix, mountpoint, undefined, backend)
        }
        entry = deps.registry.add(ws, wid)
      } catch (e) {
        await ws.close()
        return reply.status(409).send({ detail: (e as Error).message })
      }
      return reply.status(201).send(await makeDetail(entry))
    },
  )

  app.get('/v1/workspaces', () => deps.registry.list().map(makeBrief))

  app.post<{ Body: LoadWorkspaceBody }>(
    '/v1/workspaces/load',
    WRITE_RATE_LIMIT,
    async (req, reply) => {
      const { path, id: workspaceId, override } = req.body
      if (typeof path !== 'string' || path === '') {
        return reply.status(400).send({ detail: 'path is required' })
      }
      // Confinement is inlined (not via a helper) so the static analyzer sees
      // the startsWith barrier dominate the fs sink below.
      const snapshotRoot = resolve(deps.snapshotRoot)
      const safePath = resolve(snapshotRoot, path)
      if (!safePath.startsWith(snapshotRoot + sep)) {
        return reply.status(400).send({ detail: 'path escapes the configured root' })
      }
      if (workspaceId !== undefined && deps.registry.has(workspaceId)) {
        return reply.status(409).send({ detail: `workspace id already exists: ${workspaceId}` })
      }
      let tarBuf: Buffer
      try {
        tarBuf = readFileSync(safePath)
      } catch {
        return reply.status(400).send({ detail: `snapshot not found: ${path}` })
      }
      let overrides: Record<string, Resource>
      try {
        overrides = await buildOverrideResources(override ?? null)
      } catch (e) {
        return reply.status(400).send({ detail: `override build failed: ${(e as Error).message}` })
      }
      let ws: Workspace
      try {
        ws = await Workspace.load(new Uint8Array(tarBuf), {}, overrides)
      } catch (e) {
        return reply.status(400).send({ detail: `load failed: ${(e as Error).message}` })
      }
      let entry
      try {
        entry = deps.registry.add(ws, workspaceId)
      } catch (e) {
        return reply.status(409).send({ detail: (e as Error).message })
      }
      return reply.status(201).send(await makeDetail(entry))
    },
  )

  app.get<{ Params: WorkspaceIdParams; Querystring: WorkspaceGetQuery }>(
    '/v1/workspaces/:id',
    async (req, reply) => {
      const { id } = req.params
      if (!deps.registry.has(id)) return reply.status(404).send({ detail: 'workspace not found' })
      const verbose = req.query.verbose === 'true'
      return await makeDetail(deps.registry.get(id), verbose)
    },
  )

  app.delete<{ Params: WorkspaceIdParams }>('/v1/workspaces/:id', async (req, reply) => {
    const { id } = req.params
    if (!deps.registry.has(id)) return reply.status(404).send({ detail: 'workspace not found' })
    await deps.registry.remove(id)
    return { id, closedAt: Date.now() / 1000 }
  })

  app.post<{ Params: WorkspaceIdParams; Body: CloneWorkspaceBody }>(
    '/v1/workspaces/:id/clone',
    async (req, reply) => {
      const { id } = req.params
      if (!deps.registry.has(id)) return reply.status(404).send({ detail: 'workspace not found' })
      const body = req.body
      if (body.id !== undefined && deps.registry.has(body.id)) {
        return reply.status(409).send({ detail: `workspace id already exists: ${body.id}` })
      }
      const src = deps.registry.get(id).runner.ws
      const newWs = await cloneWorkspaceWithOverride(src, body.override ?? null)
      let entry
      try {
        entry = deps.registry.add(newWs, body.id)
      } catch (e) {
        return reply.status(409).send({ detail: (e as Error).message })
      }
      return reply.status(201).send(await makeDetail(entry))
    },
  )

  app.post<{ Params: WorkspaceIdParams; Body: SnapshotWorkspaceBody }>(
    '/v1/workspaces/:id/snapshot',
    WRITE_RATE_LIMIT,
    async (req, reply) => {
      const { id } = req.params
      if (!deps.registry.has(id)) return reply.status(404).send({ detail: 'workspace not found' })
      const { path } = req.body
      if (typeof path !== 'string' || path === '') {
        return reply.status(400).send({ detail: 'path is required' })
      }
      // Confinement is inlined (not via a helper) so the static analyzer sees
      // the startsWith barrier dominate the fs sink below.
      const snapshotRoot = resolve(deps.snapshotRoot)
      const safePath = resolve(snapshotRoot, path)
      if (!safePath.startsWith(snapshotRoot + sep)) {
        return reply.status(400).send({ detail: 'path escapes the configured root' })
      }
      mkdirSync(dirname(safePath), { recursive: true })
      await deps.registry.get(id).runner.ws.snapshot(safePath)
      return reply.status(200).send({ id, path: safePath, size: statSync(safePath).size })
    },
  )
}
