import type { Context } from 'hono';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { readdir, cp, rm, mkdir } from 'node:fs/promises';
import { createRouter } from '@@/create-router';
import type { HonoVariables } from '@@/create-router';
import { getUserAppsDir, getUserProjectsDir } from '@@/data-path';
import * as errors from '@@/custom-errors';
import { verify } from '@@/jwt';
import { isTokenBlacklisted } from 'officerdb';
import { rewriteHtml, rewriteAssetPaths } from '../_shared/html-rewrite';

export type AppManifest = {
  slug: string;
  name: string;
  version: string;
  icon: string;
  description: string;
  sourceProject: string;
  commitHash: string;
  publishedAt: string;
  buildDir: string;
};

// ── Protected routes (inside protectedRouter) ──

export const appsRouter = createRouter();

appsRouter.get('/', async (ctx) => {
  const user = ctx.get('user');
  const appsDir = getUserAppsDir(user.email);

  if (!existsSync(appsDir)) return ctx.json({ apps: [] });

  const entries = await readdir(appsDir, { withFileTypes: true });
  const apps: AppManifest[] = [];

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const manifestPath = join(appsDir, entry.name, 'manifest.json');
    if (!existsSync(manifestPath)) continue;
    try {
      const manifest = JSON.parse(await Bun.file(manifestPath).text()) as AppManifest;
      apps.push(manifest);
    } catch {
      // skip malformed manifests
    }
  }

  return ctx.json({ apps });
});

appsRouter.post('/publish', async (ctx) => {
  const user = ctx.get('user');
  const body = ctx.get('body') as { projectSlug: string; name?: string; icon?: string; description?: string };
  const { projectSlug, name, icon, description } = body;

  if (!projectSlug) throw errors.BAD_REQUEST('Missing projectSlug');

  const projectDir = join(getUserProjectsDir(user.email), projectSlug);
  if (!existsSync(projectDir)) throw errors.BAD_REQUEST(`Project not found: ${projectSlug}`);

  const pkgPath = join(projectDir, 'package.json');
  if (!existsSync(pkgPath)) throw errors.BAD_REQUEST('No package.json found in project');

  let pkgJson: { name?: string; version?: string; scripts?: Record<string, string> };
  try {
    pkgJson = JSON.parse(await Bun.file(pkgPath).text());
  } catch {
    throw errors.BAD_REQUEST('Failed to read package.json');
  }

  if (!pkgJson.scripts?.build) throw errors.BAD_REQUEST('No "build" script in package.json');

  const version = pkgJson.version ?? '0.1.0';
  const appSlug = projectSlug;
  const appName = name ?? pkgJson.name ?? projectSlug;

  // Read .officerdev/meta.json for fallback description
  let metaDescription = '';
  const metaPath = join(projectDir, '.officerdev', 'meta.json');
  if (existsSync(metaPath)) {
    try {
      const meta = JSON.parse(await Bun.file(metaPath).text());
      metaDescription = meta.description ?? '';
    } catch {
      // ignore
    }
  }

  // Get git commit hash
  let commitHash = 'unknown';
  try {
    const proc = Bun.spawn(['git', 'rev-parse', '--short', 'HEAD'], { cwd: projectDir, stdout: 'pipe', stderr: 'pipe' });
    await proc.exited;
    if (proc.exitCode === 0) {
      commitHash = (await new Response(proc.stdout).text()).trim();
    }
  } catch {
    // no git — that's fine
  }

  // Run build
  const buildProc = Bun.spawn(['bun', 'run', 'build'], {
    cwd: projectDir,
    stdout: 'pipe',
    stderr: 'pipe',
    env: { ...process.env, NODE_ENV: 'production' },
  });

  const buildTimeout = setTimeout(() => buildProc.kill(), 60_000);
  const exitCode = await buildProc.exited;
  clearTimeout(buildTimeout);

  if (exitCode !== 0) {
    const stderr = await new Response(buildProc.stderr).text();
    throw errors.BAD_REQUEST(`Build failed (exit ${exitCode}):\n${stderr.slice(-500)}`);
  }

  // Verify dist output
  const distDir = join(projectDir, 'dist');
  if (!existsSync(distDir)) throw errors.BAD_REQUEST('Build did not produce a dist/ directory');

  const distFiles = await readdir(distDir);
  const hasHtml = distFiles.some((f) => f.endsWith('.html'));
  if (!hasHtml) throw errors.BAD_REQUEST('Build output has no HTML files');

  // Copy to versioned directory
  const appsDir = getUserAppsDir(user.email);
  const appDir = join(appsDir, appSlug);
  const versionDir = join(appDir, 'v', version);

  await mkdir(versionDir, { recursive: true });
  await cp(distDir, versionDir, { recursive: true });

  // Write manifest
  const manifest: AppManifest = {
    slug: appSlug,
    name: appName,
    version,
    icon: icon ?? 'Box',
    description: description ?? metaDescription,
    sourceProject: projectSlug,
    commitHash,
    publishedAt: new Date().toISOString(),
    buildDir: `v/${version}`,
  };

  await Bun.write(join(appDir, 'manifest.json'), JSON.stringify(manifest, null, 2));

  return ctx.json(manifest);
});

appsRouter.delete('/:slug', async (ctx) => {
  const user = ctx.get('user');
  const slug = ctx.req.param('slug');
  if (!slug) throw errors.BAD_REQUEST('Missing slug');

  const appDir = join(getUserAppsDir(user.email), slug);
  if (!existsSync(appDir)) throw errors.NOT_FOUND('App not found');

  await rm(appDir, { recursive: true, force: true });
  return ctx.json({ ok: true });
});

// ── Static serving (outside protectedRouter) ──

export const appServeRouter = createRouter();

const MIME_TYPES: Record<string, string> = {
  '.html': 'text/html; charset=utf-8',
  '.js': 'application/javascript; charset=utf-8',
  '.mjs': 'application/javascript; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.ico': 'image/x-icon',
  '.webp': 'image/webp',
  '.woff': 'font/woff',
  '.woff2': 'font/woff2',
  '.ttf': 'font/ttf',
  '.eot': 'application/vnd.ms-fontobject',
  '.map': 'application/json',
};

const getMimeType = (filePath: string): string => {
  const ext = filePath.slice(filePath.lastIndexOf('.'));
  return MIME_TYPES[ext] ?? 'application/octet-stream';
};

type ValidateResult = { email: string } | null;

async function validateToken(req: Request): Promise<ValidateResult> {
  const authHeader = req.headers.get('authorization');
  const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : new URL(req.url).searchParams.get('token');
  if (!token) return null;
  try {
    const payload = await verify(token);
    if (!payload?.email) return null;
    if (payload.jti && (await isTokenBlacklisted(payload.jti))) return null;
    return { email: payload.email };
  } catch {
    return null;
  }
}

appServeRouter.all('/:slug/*', async (ctx) => {
  const slug = ctx.req.param('slug');
  if (!slug) return ctx.text('Not found', 404);

  // Validate auth — HTML requests require token, assets check referer
  const pathname = new URL(ctx.req.url).pathname;
  const isHtml =
    ctx.req.header('accept')?.includes('text/html') ||
    pathname.endsWith('.html') ||
    !pathname.slice(pathname.lastIndexOf('/') + 1).includes('.');

  const validated = await validateToken(ctx.req.raw);

  if (!validated) {
    // For non-HTML assets, try referer-based resolution
    if (!isHtml) {
      const referer = ctx.req.header('referer');
      if (referer) {
        const match = referer.match(/\/api\/app-serve\/([^/?]+)/);
        if (match) {
          // Extract token from referer URL if present
          try {
            const refUrl = new URL(referer);
            const refToken = refUrl.searchParams.get('token');
            if (refToken) {
              const payload = await verify(refToken);
              if (payload?.email) {
                if (!payload.jti || !(await isTokenBlacklisted(payload.jti))) {
                  return serveFile(ctx, payload.email, slug);
                }
              }
            }
          } catch {
            // fall through
          }
        }
      }
    }
    return ctx.text('Unauthorized', 401);
  }

  return serveFile(ctx, validated.email, slug);
});

async function serveFile(ctx: Context<{ Variables: HonoVariables }>, email: string, slug: string) {
  const appDir = join(getUserAppsDir(email), slug);
  const manifestPath = join(appDir, 'manifest.json');

  if (!existsSync(manifestPath)) return ctx.text('App not found', 404);

  let manifest: AppManifest;
  try {
    manifest = JSON.parse(await Bun.file(manifestPath).text());
  } catch {
    return ctx.text('Invalid manifest', 500);
  }

  const buildDir = join(appDir, manifest.buildDir);
  const prefix = `/api/app-serve/${slug}`;

  const url = new URL(ctx.req.url);
  let filePath = url.pathname.replace(prefix, '') || '/';
  if (filePath === '/') filePath = '/index.html';

  const fullPath = join(buildDir, filePath);

  // Security: prevent directory traversal
  if (!fullPath.startsWith(buildDir)) return ctx.text('Forbidden', 403);

  // If file doesn't exist, serve index.html for SPA routing
  const targetPath = existsSync(fullPath) ? fullPath : join(buildDir, 'index.html');
  if (!existsSync(targetPath)) return ctx.text('Not found', 404);

  const file = Bun.file(targetPath);
  const mimeType = getMimeType(targetPath);
  const needsRewrite =
    mimeType.includes('text/html') || mimeType.includes('javascript') || mimeType.includes('text/css');

  if (needsRewrite) {
    const content = await file.text();
    const rewritten = mimeType.includes('text/html')
      ? rewriteHtml(content, prefix)
      : rewriteAssetPaths(content, prefix);
    return new Response(rewritten, { headers: { 'content-type': mimeType } });
  }

  return new Response(file, { headers: { 'content-type': mimeType } });
}
