import { createReadStream, existsSync, statSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, join, normalize } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = fileURLToPath(new URL('.', import.meta.url))
const rootDir = normalize(join(__dirname, '..'))
const publicDir = join(rootDir, 'public')
const assetDir = join(publicDir, 'action-assets')
const host = process.env.HOST ?? '127.0.0.1'
const port = Number(process.env.PORT ?? 4175)

const assetSlots = [
  {
    id: 'dungeon-bulldrome',
    category: 'Dungeon Icons',
    label: 'Bulldrome Hunting Grounds',
    path: 'dungeons/bulldrome.svg',
    preview: 'dungeon',
  },
  {
    id: 'dungeon-yian-kut-ku',
    category: 'Dungeon Icons',
    label: 'Yian Kut-Ku Roost',
    path: 'dungeons/yian-kut-ku.svg',
    preview: 'dungeon',
  },
  {
    id: 'dungeon-cyber-dragon',
    category: 'Dungeon Icons',
    label: 'Cyber Dragon Core',
    path: 'dungeons/cyber-dragon.svg',
    preview: 'dungeon',
  },
  {
    id: 'enemy-cyber-dragon',
    category: 'Boss and Mob Sprites',
    label: 'Cyber Dragon',
    path: 'enemies/cyber-dragon.svg',
    preview: 'enemy',
  },
  {
    id: 'enemy-bulldrome',
    category: 'Boss and Mob Sprites',
    label: 'Bulldrome',
    path: 'enemies/bulldrome.svg',
    preview: 'enemy',
  },
  {
    id: 'enemy-bullfango',
    category: 'Boss and Mob Sprites',
    label: 'Bullfango',
    path: 'enemies/bullfango.svg',
    preview: 'enemy',
  },
  {
    id: 'enemy-yian-kut-ku',
    category: 'Boss and Mob Sprites',
    label: 'Yian Kut-Ku',
    path: 'enemies/yian-kut-ku.svg',
    preview: 'enemy',
  },
  {
    id: 'enemy-bird',
    category: 'Boss and Mob Sprites',
    label: 'Bird',
    path: 'enemies/bird.svg',
    preview: 'enemy',
  },
]

const mimeTypes = {
  '.css': 'text/css; charset=utf-8',
  '.html': 'text/html; charset=utf-8',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.js': 'text/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.svg': 'image/svg+xml; charset=utf-8',
  '.webp': 'image/webp',
}

const server = createServer(async (request, response) => {
  try {
    const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`)

    if (request.method === 'GET' && url.pathname === '/') {
      sendHtml(response, adminHtml())
      return
    }

    if (request.method === 'GET' && url.pathname === '/api/assets') {
      sendJson(response, 200, { slots: assetSlots.map(slotPayload) })
      return
    }

    if (request.method === 'POST' && url.pathname.startsWith('/api/assets/')) {
      const slotId = decodeURIComponent(url.pathname.replace('/api/assets/', ''))
      const slot = assetSlots.find((item) => item.id === slotId)
      if (!slot) throw statusError(404, 'Unknown asset slot.')

      const body = await readJson(request, 16 * 1024 * 1024)
      const svg = normalizeUploadToSvg(body)
      const targetPath = join(assetDir, slot.path)
      await mkdir(join(targetPath, '..'), { recursive: true })
      await writeFile(targetPath, svg)
      sendJson(response, 200, { slot: slotPayload(slot) })
      return
    }

    if (request.method === 'GET' && url.pathname.startsWith('/action-assets/')) {
      await servePublicAsset(response, url.pathname)
      return
    }

    sendJson(response, 404, { error: 'not_found' })
  } catch (error) {
    const status = Number(error?.status) || 500
    if (status >= 500) console.error(error)
    sendJson(response, status, {
      error: error instanceof Error ? error.message : 'Unable to process request.',
    })
  }
})

server.listen(port, host, () => {
  console.log(`Action asset admin listening on http://${host}:${port}`)
})

function slotPayload(slot) {
  const filePath = join(assetDir, slot.path)
  const stat = existsSync(filePath) ? statSync(filePath) : null
  return {
    ...slot,
    size: stat?.size ?? 0,
    updatedAt: stat?.mtime.toISOString() ?? null,
    url: `/action-assets/${slot.path}`,
  }
}

function normalizeUploadToSvg(body) {
  const dataUrl = String(body?.dataUrl ?? '')
  const width = clampDimension(Number(body?.width), 256)
  const height = clampDimension(Number(body?.height), 256)
  const originalName = escapeXml(String(body?.name ?? 'uploaded-image').slice(0, 120))
  const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|svg\+xml));base64,([A-Za-z0-9+/=]+)$/)
  if (!match) throw statusError(400, 'Upload must be PNG, JPG, WebP, or SVG.')

  const decodedBytes = Buffer.byteLength(match[2], 'base64')
  if (decodedBytes > 10 * 1024 * 1024) throw statusError(400, 'Upload must be 10 MB or smaller.')

  return [
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="${originalName}">`,
    `  <image href="${dataUrl}" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>`,
    '</svg>',
    '',
  ].join('\n')
}

function clampDimension(value, fallback) {
  if (!Number.isFinite(value) || value < 1) return fallback
  return Math.min(4096, Math.round(value))
}

async function readJson(request, maxSize) {
  const chunks = []
  let size = 0
  for await (const chunk of request) {
    size += chunk.length
    if (size > maxSize) throw statusError(400, 'Request body is too large.')
    chunks.push(chunk)
  }
  return JSON.parse(Buffer.concat(chunks).toString('utf8'))
}

async function servePublicAsset(response, pathname) {
  const safePath = normalize(pathname).replace(/^(\.\.[/\\])+/, '')
  const filePath = join(publicDir, safePath)
  if (!filePath.startsWith(publicDir) || !existsSync(filePath) || !statSync(filePath).isFile()) {
    sendJson(response, 404, { error: 'not_found' })
    return
  }

  response.writeHead(200, {
    'Cache-Control': 'no-store',
    'Content-Type': mimeTypes[extname(filePath)] ?? 'application/octet-stream',
  })
  createReadStream(filePath).pipe(response)
}

function sendHtml(response, html) {
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
  response.end(html)
}

function sendJson(response, status, payload) {
  response.writeHead(status, {
    'Cache-Control': 'no-store',
    'Content-Type': 'application/json; charset=utf-8',
  })
  response.end(JSON.stringify(payload))
}

function statusError(status, message) {
  const error = new Error(message)
  error.status = status
  return error
}

function escapeXml(value) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
}

function adminHtml() {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Action Asset Admin</title>
    <style>
      :root {
        --ink: #f4eed8;
        --muted: #a89f87;
        --panel: #191b22;
        --panel-light: #242630;
        --edge: #565066;
        --gold: #e5b95f;
        --red: #dc5162;
        --green: #76d39a;
        --blue: #5ec7ff;
      }

      * { box-sizing: border-box; }
      body {
        background: #07080b;
        color: var(--ink);
        font-family: Arial, sans-serif;
        margin: 0;
      }
      button, input { font: inherit; }
      .admin-shell {
        display: grid;
        gap: 14px;
        grid-template-columns: minmax(320px, 0.85fr) minmax(520px, 1.15fr);
        min-height: 100vh;
        padding: 14px;
      }
      .panel {
        background: var(--panel);
        border: 2px solid #090a0d;
        outline: 2px solid var(--edge);
        min-width: 0;
      }
      .asset-panel {
        overflow: auto;
        padding: 14px;
      }
      h1, h2, h3, p { margin: 0; }
      h1, h2, h3, .eyebrow, .asset-card strong, .pixel {
        font-family: "Courier New", monospace;
        font-weight: 800;
        letter-spacing: 0;
        text-transform: uppercase;
      }
      h1 { font-size: 24px; line-height: 1.2; }
      h2 { font-size: 15px; margin: 18px 0 8px; }
      .eyebrow {
        color: var(--gold);
        font-size: 11px;
        margin-bottom: 8px;
      }
      .copy {
        color: var(--muted);
        line-height: 1.35;
        margin-top: 8px;
      }
      .asset-list {
        display: grid;
        gap: 10px;
      }
      .asset-card {
        align-items: center;
        background: #111319;
        border: 2px solid #090a0d;
        display: grid;
        gap: 10px;
        grid-template-columns: 72px minmax(0, 1fr);
        outline: 2px solid #41404a;
        padding: 10px;
      }
      .asset-card.selected {
        outline-color: var(--gold);
      }
      .asset-card img {
        background: #08090c;
        border: 2px solid #090a0d;
        display: block;
        height: 72px;
        object-fit: contain;
        width: 72px;
      }
      .asset-card strong {
        display: block;
        font-size: 12px;
        line-height: 1.25;
      }
      .asset-card small {
        color: var(--muted);
        display: block;
        font-size: 12px;
        margin-top: 4px;
        word-break: break-all;
      }
      .asset-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 8px;
      }
      .asset-actions input {
        color: var(--muted);
        max-width: 100%;
      }
      .asset-actions button, .toolbar a {
        background: var(--gold);
        border: 2px solid #08090c;
        color: #19150e;
        cursor: pointer;
        outline: 2px solid #816630;
        padding: 8px 10px;
        text-decoration: none;
      }
      .asset-actions button:disabled {
        cursor: wait;
        opacity: 0.55;
      }
      .toolbar {
        align-items: center;
        display: flex;
        gap: 10px;
        justify-content: space-between;
        margin-top: 12px;
      }
      .status {
        color: var(--green);
        min-height: 20px;
      }
      .status.error {
        color: #ff8190;
      }
      .preview-panel {
        display: grid;
        gap: 14px;
        grid-template-rows: auto auto minmax(0, 1fr);
        overflow: hidden;
        padding: 14px;
      }
      .dungeon-preview-grid {
        display: grid;
        gap: 10px;
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }
      .dungeon-card {
        align-items: center;
        background: #111319;
        border: 2px solid #090a0d;
        display: grid;
        gap: 8px;
        grid-template-columns: 56px minmax(0, 1fr);
        min-height: 80px;
        outline: 2px solid #41404a;
        padding: 8px;
      }
      .dungeon-card img {
        background: #171922;
        border: 2px solid #090a0d;
        display: block;
        height: 56px;
        object-fit: cover;
        width: 56px;
      }
      .dungeon-card strong {
        display: block;
        font-size: 11px;
        line-height: 1.2;
      }
      .dungeon-card small {
        color: var(--muted);
        display: block;
        line-height: 1.2;
        margin-top: 4px;
      }
      .fight-preview-shell {
        align-items: center;
        background: #08090c;
        border: 2px solid #090a0d;
        display: flex;
        justify-content: center;
        min-height: 340px;
        outline: 2px solid #41404a;
        overflow: hidden;
        padding: 12px;
      }
      .fight-preview {
        aspect-ratio: 16 / 9;
        background: #11151c;
        max-height: 100%;
        max-width: 100%;
        position: relative;
        width: 100%;
      }
      .arena {
        background:
          linear-gradient(rgba(37,43,53,.65) 1px, transparent 1px),
          linear-gradient(90deg, rgba(37,43,53,.65) 1px, transparent 1px),
          #18202a;
        background-size: 48px 48px;
        border: 3px solid var(--edge);
        inset: 13% 4% 7%;
        position: absolute;
      }
      .bossbar {
        left: 50%;
        position: absolute;
        top: 12px;
        transform: translateX(-50%);
        width: min(440px, calc(100% - 40px));
        z-index: 3;
      }
      .bossbar strong, .bossbar span {
        display: block;
        font-family: "Courier New", monospace;
        font-size: 12px;
        font-weight: 800;
        text-align: center;
      }
      .bossbar span {
        color: var(--muted);
        margin-top: 2px;
      }
      .bossbar i {
        background: #090a0d;
        border: 2px solid #090a0d;
        display: block;
        height: 16px;
        margin-top: 4px;
      }
      .bossbar b {
        background: var(--red);
        display: block;
        height: 100%;
        width: 82%;
      }
      .unit {
        position: absolute;
        transform: translate(-50%, -50%);
        z-index: 2;
      }
      .unit img {
        display: block;
        height: 78px;
        object-fit: contain;
        width: 78px;
      }
      .unit.bulldrome img, .unit.yian-kut-ku img {
        height: 116px;
        width: 116px;
      }
      .player-dot {
        background: #30ff7a;
        border: 3px solid #090a0d;
        border-radius: 50%;
        height: 28px;
        left: 50%;
        position: absolute;
        top: 74%;
        transform: translate(-50%, -50%);
        width: 28px;
        z-index: 2;
      }
      @media (max-width: 920px) {
        .admin-shell {
          grid-template-columns: 1fr;
        }
      }
    </style>
  </head>
  <body>
    <main class="admin-shell">
      <section class="panel asset-panel">
        <p class="eyebrow">Local Only</p>
        <h1>Action Asset Admin</h1>
        <p class="copy">Uploads write directly into <span class="pixel">public/action-assets</span>. Next web or mobile build uses them automatically.</p>
        <div class="toolbar">
          <p id="status" class="status" aria-live="polite"></p>
          <a href="http://127.0.0.1:5173/" target="_blank" rel="noreferrer">Open Game</a>
        </div>
        <div id="assetRoot"></div>
      </section>

      <section class="panel preview-panel">
        <div>
          <p class="eyebrow">In-Game Preview</p>
          <h1>Dungeon Cards</h1>
        </div>
        <div class="dungeon-preview-grid" id="dungeonPreview"></div>
        <div>
          <p class="eyebrow">Combat Preview</p>
          <div class="fight-preview-shell">
            <div class="fight-preview">
              <div class="arena"></div>
              <div class="bossbar">
                <strong>Boss Pack</strong>
                <span>364 / 375</span>
                <i><b></b></i>
              </div>
              <div id="enemyPreview"></div>
              <div class="player-dot" title="Player"></div>
            </div>
          </div>
        </div>
      </section>
    </main>

    <script>
      const state = {
        slots: [],
        selectedId: '',
        pending: new Map(),
      }

      const positions = {
        'enemy-bulldrome': ['50%', '31%', 'bulldrome'],
        'enemy-bullfango': ['50%', '33%', 'bullfango'],
        'enemy-yian-kut-ku': ['50%', '31%', 'yian-kut-ku'],
        'enemy-bird': ['64%', '45%', 'bird'],
        'enemy-cyber-dragon': ['50%', '31%', 'cyber-dragon'],
      }

      async function loadSlots() {
        const response = await fetch('/api/assets')
        const body = await response.json()
        state.slots = body.slots
        state.selectedId = state.selectedId || state.slots[0]?.id || ''
        render()
      }

      function assetUrl(slot) {
        const pending = state.pending.get(slot.id)
        if (pending) return pending.dataUrl
        return slot.url + '?v=' + encodeURIComponent(slot.updatedAt || Date.now())
      }

      function render() {
        renderAssetList()
        renderDungeonPreview()
        renderEnemyPreview()
      }

      function renderAssetList() {
        const root = document.getElementById('assetRoot')
        const groups = state.slots.reduce((map, slot) => {
          if (!map.has(slot.category)) map.set(slot.category, [])
          map.get(slot.category).push(slot)
          return map
        }, new Map())
        root.innerHTML = Array.from(groups, ([category, slots]) => \`
          <h2>\${category}</h2>
          <div class="asset-list">
            \${slots.map(slot => assetCard(slot)).join('')}
          </div>
        \`).join('')

        for (const slot of state.slots) {
          document.getElementById('file-' + slot.id).addEventListener('change', event => selectFile(slot, event))
          document.getElementById('save-' + slot.id).addEventListener('click', () => saveSlot(slot))
        }
      }

      function assetCard(slot) {
        const pending = state.pending.get(slot.id)
        return \`
          <article class="asset-card \${slot.id === state.selectedId ? 'selected' : ''}" data-slot="\${slot.id}">
            <img src="\${assetUrl(slot)}" alt="">
            <div>
              <strong>\${slot.label}</strong>
              <small>\${slot.path}</small>
              <small>\${slot.updatedAt ? 'Updated ' + new Date(slot.updatedAt).toLocaleString() : 'Missing file'}\${pending ? ' - pending' : ''}</small>
              <div class="asset-actions">
                <input id="file-\${slot.id}" type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml">
                <button id="save-\${slot.id}" type="button" \${pending ? '' : 'disabled'}>Save</button>
              </div>
            </div>
          </article>
        \`
      }

      function renderDungeonPreview() {
        const root = document.getElementById('dungeonPreview')
        const dungeons = state.slots.filter(slot => slot.preview === 'dungeon')
        root.innerHTML = dungeons.map(slot => \`
          <article class="dungeon-card">
            <img src="\${assetUrl(slot)}" alt="">
            <div>
              <strong>\${slot.label}</strong>
              <small>Action dungeon icon preview.</small>
            </div>
          </article>
        \`).join('')
      }

      function renderEnemyPreview() {
        const root = document.getElementById('enemyPreview')
        const enemies = state.slots.filter(slot => slot.preview === 'enemy')
        root.innerHTML = enemies.map(slot => {
          const [left, top, className] = positions[slot.id] || ['50%', '50%', '']
          return \`
            <div class="unit \${className}" style="left: \${left}; top: \${top}">
              <img src="\${assetUrl(slot)}" alt="\${slot.label}">
            </div>
          \`
        }).join('')
      }

      function selectFile(slot, event) {
        const file = event.target.files?.[0]
        if (!file) return
        if (!/^image\\/(png|jpeg|webp|svg\\+xml)$/.test(file.type)) {
          setStatus('Use PNG, JPG, WebP, or SVG.', true)
          return
        }

        const reader = new FileReader()
        reader.onload = () => {
          const dataUrl = String(reader.result)
          const image = new Image()
          image.onload = () => {
            state.pending.set(slot.id, {
              dataUrl,
              height: image.naturalHeight || 256,
              name: file.name,
              width: image.naturalWidth || 256,
            })
            state.selectedId = slot.id
            setStatus('Preview updated. Save to write file.', false)
            render()
          }
          image.onerror = () => setStatus('Could not read image.', true)
          image.src = dataUrl
        }
        reader.readAsDataURL(file)
      }

      async function saveSlot(slot) {
        const pending = state.pending.get(slot.id)
        if (!pending) return
        setStatus('Saving...', false)
        const response = await fetch('/api/assets/' + encodeURIComponent(slot.id), {
          body: JSON.stringify(pending),
          headers: { 'Content-Type': 'application/json' },
          method: 'POST',
        })
        const body = await response.json()
        if (!response.ok) {
          setStatus(body.error || 'Save failed.', true)
          return
        }
        state.pending.delete(slot.id)
        const index = state.slots.findIndex(item => item.id === slot.id)
        if (index >= 0) state.slots[index] = body.slot
        setStatus('Saved. Next build will include this asset.', false)
        render()
      }

      function setStatus(message, error) {
        const element = document.getElementById('status')
        element.textContent = message
        element.classList.toggle('error', Boolean(error))
      }

      loadSlots().catch(error => setStatus(error.message, true))
    </script>
  </body>
</html>`
}
