import { type Application, type Request, type Response, type RequestHandler } from 'express';
import express from 'express';
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
import { join, extname, basename } from 'path';
import { randomBytes } from 'crypto';
import type { ConfigManager } from '../config-manager.js';
import { logger } from '../logger.js';
import { setUntrustedFileResponseHeaders } from './local-api-helpers.js';

export interface PublicBranding {
  appName: string;
  primaryColor: string;
  loginPageTitle: string;
  logoUrl: string | null;
  faviconUrl: string | null;
  footerText: string | null;
}

const DEFAULTS: PublicBranding = {
  appName: 'MAESTRO',
  primaryColor: '#2563eb',
  loginPageTitle: 'MAESTRO',
  logoUrl: null,
  faviconUrl: null,
  footerText: null,
};

function pickString(obj: Record<string, unknown>, key: string): string | null {
  const v = obj[key];
  if (typeof v === 'string' && v.trim().length > 0) return v.trim();
  return null;
}

export function resolveBranding(configManager: ConfigManager | undefined): PublicBranding {
  const raw = configManager?.getConfigForApi()?.config?.branding;
  const cfg: Record<string, unknown> =
    raw && typeof raw === 'object' && !Array.isArray(raw)
      ? (raw as Record<string, unknown>)
      : {};
  const appName = pickString(cfg, 'appName') ?? DEFAULTS.appName;
  const primaryColor = pickString(cfg, 'primaryColor') ?? DEFAULTS.primaryColor;
  const loginPageTitle = pickString(cfg, 'loginPageTitle') ?? appName;
  const logoUrl = pickString(cfg, 'logoUrl');
  const faviconUrl = pickString(cfg, 'faviconUrl');
  const footerText = pickString(cfg, 'footerText');
  return { appName, primaryColor, loginPageTitle, logoUrl, faviconUrl, footerText };
}

// ── Upload handling ──────────────────────────────────────────────────────────

type AssetKind = 'logo' | 'favicon';
const ALLOWED_KINDS: AssetKind[] = ['logo', 'favicon'];

const ALLOWED_EXTENSIONS: Record<AssetKind, string[]> = {
  logo: ['.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif'],
  favicon: ['.svg', '.png', '.ico', '.webp'],
};

const MAX_SIZE_BYTES: Record<AssetKind, number> = {
  logo: 2 * 1024 * 1024,       // 2 MB
  favicon: 256 * 1024,          // 256 KB
};

function sanitizeExt(filename: string): string {
  const ext = extname(filename).toLowerCase();
  // Defensive: reject anything with path separators / null bytes
  if (/[\\/\0]/.test(ext)) return '';
  // Extension must be in a safe set
  if (!/^\.[a-z0-9]{1,5}$/.test(ext)) return '';
  return ext;
}

function removeExistingAsset(brandingDir: string, kind: AssetKind): void {
  if (!existsSync(brandingDir)) return;
  for (const name of readdirSync(brandingDir)) {
    if (name.startsWith(`${kind}-`)) {
      try {
        unlinkSync(join(brandingDir, name));
      } catch (e) {
        logger.warn(`[branding] failed to remove old asset ${name}: ${e}`);
      }
    }
  }
}

function assetUrlFromConfig(configManager: ConfigManager | undefined, kind: AssetKind): string | null {
  const b = configManager?.getConfigForApi()?.config?.branding;
  if (!b || typeof b !== 'object') return null;
  const key = kind === 'logo' ? 'logoUrl' : 'faviconUrl';
  const v = (b as Record<string, unknown>)[key];
  return typeof v === 'string' ? v : null;
}

function removeAssetByUrl(brandingDir: string, url: string | null): void {
  if (!url) return;
  if (!url.startsWith('/branding/')) return;
  const name = basename(url);
  // Defense in depth: basename strips any `..` components
  if (!name || name.includes('..')) return;
  const fullPath = join(brandingDir, name);
  if (existsSync(fullPath)) {
    try {
      unlinkSync(fullPath);
    } catch (e) {
      logger.warn(`[branding] failed to remove asset ${name}: ${e}`);
    }
  }
}

export interface MountBrandingOptions {
  /** Absolute or relative path where branding assets (logos, favicons) are stored. Created on demand. */
  brandingDir: string;
  /** Admin-only middleware. When auth is disabled, pass a passthrough. */
  adminGuard: RequestHandler;
}

export function mountBrandingApi(
  app: Application,
  configManager: ConfigManager | undefined,
  opts?: MountBrandingOptions,
): void {
  // Public GET — no auth required. UI fetches this at startup (even on login page).
  app.get('/api/branding', (_req: Request, res: Response) => {
    res.json(resolveBranding(configManager));
  });

  if (!opts) return;

  const { brandingDir, adminGuard } = opts;

  // Serve uploaded assets. Directory is created lazily if first write happens;
  // express.static handles the not-exists case by falling through to 404.
  // Assets are admin-uploaded and the allowlist includes .svg, which can embed
  // scripts: navigating to /branding/logo.svg directly would otherwise run that
  // script on the app origin (admin stored XSS). `CSP: sandbox` + nosniff
  // neutralize this while leaving <img src> subresource rendering untouched.
  app.use('/branding', express.static(brandingDir, {
    maxAge: '7d',
    fallthrough: true,
    setHeaders: (res) => setUntrustedFileResponseHeaders(res),
  }));

  // Upload endpoint: admin only. Body is JSON with base64 content
  // (same pattern as task attachment upload in local-tasks-api.ts).
  const uploadJson = express.json({ limit: '4mb' });

  app.post('/api/branding/upload', uploadJson, adminGuard, (req: Request, res: Response) => {
    if (!configManager) {
      res.status(503).json({ ok: false, error: 'ConfigManager unavailable' });
      return;
    }
    try {
      const { kind, filename, contentBase64 } = req.body ?? {};
      if (!ALLOWED_KINDS.includes(kind)) {
        res.status(400).json({ ok: false, error: 'kind must be "logo" or "favicon"' });
        return;
      }
      if (typeof filename !== 'string' || typeof contentBase64 !== 'string') {
        res.status(400).json({ ok: false, error: 'filename and contentBase64 are required strings' });
        return;
      }
      const ext = sanitizeExt(filename);
      if (!ext || !ALLOWED_EXTENSIONS[kind as AssetKind].includes(ext)) {
        res.status(400).json({
          ok: false,
          error: `extension must be one of ${ALLOWED_EXTENSIONS[kind as AssetKind].join(', ')}`,
        });
        return;
      }

      let buf: Buffer;
      try {
        buf = Buffer.from(contentBase64, 'base64');
      } catch {
        res.status(400).json({ ok: false, error: 'invalid base64 content' });
        return;
      }
      if (buf.length === 0) {
        res.status(400).json({ ok: false, error: 'empty file' });
        return;
      }
      if (buf.length > MAX_SIZE_BYTES[kind as AssetKind]) {
        res.status(413).json({
          ok: false,
          error: `file too large (max ${MAX_SIZE_BYTES[kind as AssetKind]} bytes)`,
        });
        return;
      }

      // Create branding directory on first upload
      if (!existsSync(brandingDir)) {
        mkdirSync(brandingDir, { recursive: true });
      }

      // Clean up any previously stored asset of this kind so we don't
      // accumulate orphaned files when the admin re-uploads.
      removeExistingAsset(brandingDir, kind as AssetKind);

      // Hash-suffixed filename provides unique URL for cache busting
      const hash = randomBytes(6).toString('hex');
      const storedName = `${kind}-${hash}${ext}`;
      writeFileSync(join(brandingDir, storedName), buf);

      const publicUrl = `/branding/${storedName}`;
      const configKey = kind === 'logo' ? 'logoUrl' : 'faviconUrl';
      const result = configManager.updateConfig({ branding: { [configKey]: publicUrl } });
      if (!result.ok) {
        // Rollback: delete the file we just wrote, otherwise the config and
        // filesystem disagree forever.
        try { unlinkSync(join(brandingDir, storedName)); } catch { /* best effort */ }
        res.status(500).json({ ok: false, error: 'Failed to persist config', detail: result });
        return;
      }

      logger.info(`[branding] uploaded ${kind} -> ${publicUrl} (${buf.length} bytes)`);
      res.json({ ok: true, kind, url: publicUrl });
    } catch (e) {
      logger.warn(`[branding] upload failed: ${e}`);
      res.status(500).json({ ok: false, error: String(e) });
    }
  });

  app.delete('/api/branding/upload', adminGuard, (req: Request, res: Response) => {
    if (!configManager) {
      res.status(503).json({ ok: false, error: 'ConfigManager unavailable' });
      return;
    }
    try {
      const kind = req.query.kind;
      if (typeof kind !== 'string' || !ALLOWED_KINDS.includes(kind as AssetKind)) {
        res.status(400).json({ ok: false, error: 'query.kind must be "logo" or "favicon"' });
        return;
      }
      const currentUrl = assetUrlFromConfig(configManager, kind as AssetKind);
      removeAssetByUrl(brandingDir, currentUrl);

      const configKey = kind === 'logo' ? 'logoUrl' : 'faviconUrl';
      const result = configManager.updateConfig({ branding: { [configKey]: '' } });
      if (!result.ok) {
        res.status(500).json({ ok: false, error: 'Failed to persist config', detail: result });
        return;
      }
      logger.info(`[branding] cleared ${kind}`);
      res.json({ ok: true });
    } catch (e) {
      logger.warn(`[branding] delete failed: ${e}`);
      res.status(500).json({ ok: false, error: String(e) });
    }
  });
}
