import {
  createCapabilityToken,
  jsonResponse,
  parseCapabilityToken,
  wantsJSON,
  type CreateTrayRequest,
  type DurableObjectNamespaceLike,
} from './shared.js';
import { buildHandoffResponse } from './handoff-page.js';
import { SessionTrayDurableObject } from './session-tray.js';
import {
  handleOAuthToken,
  handleOAuthRevoke,
  handleOAuthPreflight,
  handleOAuthMethodNotAllowed,
} from './oauth-exchange.js';
import { applySliccLinks } from './links.js';
import { buildApiCatalogResponse } from './api-catalog.js';
import { buildLlmsTxtResponse } from './llms-txt.js';
import { buildRelResponse } from './rel-docs.js';

export interface WorkerEnv {
  TRAY_HUB: DurableObjectNamespaceLike;
  ASSETS: { fetch(request: Request): Promise<Response> };
  CLOUDFLARE_TURN_KEY_ID?: string;
  CLOUDFLARE_TURN_API_TOKEN?: string;
}

function serveSPA(request: Request, env: WorkerEnv): Promise<Response> {
  return env.ASSETS.fetch(request);
}
const OAUTH_RELAY_HTML = `<!DOCTYPE html>
<html><head><title>Redirecting to SLICC...</title></head>
<body>
<p id="msg">Redirecting to SLICC...</p>
<script>
try {
  var params = new URLSearchParams(location.search);
  var hashParams = new URLSearchParams(location.hash.replace(/^#/, ''));
  var raw = params.get('state') || hashParams.get('state');
  if (!raw) throw new Error('Missing state parameter');
  var state = JSON.parse(atob(raw));
  var source = state.source || 'local';
  var path = state.path || '/auth/callback';
  var nonce = state.nonce || '';
  if (!path.startsWith('/')) throw new Error('Invalid path');
  // Forward all original query params (except state, which we consumed) so
  // authorization codes (?code=xxx) survive the relay.
  params.delete('state');
  params.set('nonce', nonce);
  var query = '?' + params.toString();
  var target;
  if (source === 'local') {
    var port = Number(state.port);
    if (!port || port < 1024 || port > 65535) throw new Error('Invalid port: ' + port);
    target = 'http://localhost:' + port + path + query;
  } else if (source === 'extension') {
    // Chrome extension IDs are 32 chars in [a-p]. Strict format check prevents
    // open-redirect via subdomain injection (e.g. "evil.com.").
    var extensionId = state.extensionId || '';
    if (!/^[a-p]{32}$/.test(extensionId)) throw new Error('Invalid extensionId');
    target = 'https://' + extensionId + '.chromiumapp.org' + path + query;
  } else {
    throw new Error('Unknown source: ' + source);
  }
  location.replace(target + location.hash);
} catch (e) {
  document.getElementById('msg').textContent = 'OAuth redirect failed: ' + e.message + '. Close this window and try again.';
}
</script>
</body></html>`;

export async function handleWorkerRequest(
  request: Request,
  env: WorkerEnv,
  fetchImpl: typeof fetch = fetch
): Promise<Response> {
  const url = new URL(request.url);

  if (url.hostname === 'sliccy.ai') {
    const target = new URL(url.toString());
    target.hostname = 'www.sliccy.ai';
    return Response.redirect(target.toString(), 301);
  }

  if (url.pathname === '/tray' && request.method === 'POST') {
    return createTray(request, env);
  }

  if ((url.pathname === '/session' || url.pathname === '/trays') && request.method === 'POST') {
    return jsonResponse(
      {
        error: 'Tray creation moved to POST /tray',
        code: 'TRAY_CREATE_ENDPOINT_MOVED',
        canonical: 'POST /tray',
      },
      410
    );
  }

  // OAuth callback relay — serves a static HTML page that reads the OAuth state
  // parameter and redirects to the correct localhost port. Provider-agnostic.
  if (url.pathname === '/auth/callback') {
    return new Response(OAUTH_RELAY_HTML, {
      status: 200,
      headers: { 'Content-Type': 'text/html; charset=utf-8' },
    });
  }

  // Generic OAuth token exchange and revocation (authorization code grant)
  if (url.pathname === '/oauth/token' || url.pathname === '/oauth/revoke') {
    if (request.method === 'OPTIONS') {
      return handleOAuthPreflight(request);
    }
    if (request.method !== 'POST') {
      return handleOAuthMethodNotAllowed(request);
    }
    if (url.pathname === '/oauth/token') {
      return handleOAuthToken(request, env as unknown as Record<string, unknown>, fetchImpl);
    }
    return handleOAuthRevoke(request, env as unknown as Record<string, unknown>, fetchImpl);
  }

  // Serve runtime config for the webapp (when served from the worker).
  // CORS enabled so dev-mode apps on localhost can fetch env-specific config.
  if (url.pathname === '/api/runtime-config') {
    const workerBaseUrl = `${url.protocol}//${url.host}`;
    const envRecord = env as unknown as Record<string, unknown>;
    const origin = request.headers.get('Origin');
    const cors: Record<string, string> = origin
      ? { 'Access-Control-Allow-Origin': origin, Vary: 'Origin' }
      : {};
    return jsonResponse(
      {
        trayWorkerBaseUrl: workerBaseUrl,
        // Expose public OAuth client IDs so the webapp can build authorize URLs
        // for the correct environment (staging vs production).
        oauth: {
          github:
            typeof envRecord.GITHUB_CLIENT_ID === 'string' ? envRecord.GITHUB_CLIENT_ID : undefined,
        },
      },
      200,
      cors
    );
  }

  // Fetch proxy not available in worker mode (webapp uses direct fetch instead)
  if (url.pathname === '/api/fetch-proxy') {
    return jsonResponse({ error: 'Fetch proxy not available in worker mode' }, 404);
  }

  if (
    url.pathname === '/download/slicc.dmg' &&
    (request.method === 'GET' || request.method === 'HEAD')
  ) {
    return handleDmgDownload();
  }

  if (url.pathname === '/handoff' && request.method === 'GET') {
    return buildHandoffResponse(request);
  }

  if (
    url.pathname === '/.well-known/api-catalog' &&
    (request.method === 'GET' || request.method === 'HEAD')
  ) {
    return buildApiCatalogResponse(request);
  }

  if (url.pathname === '/llms.txt' && (request.method === 'GET' || request.method === 'HEAD')) {
    return buildLlmsTxtResponse(request);
  }

  // Public health endpoint. Advertised via the `status` rel (RFC 8631) in the
  // standard Link header set, so any consumer that walks the rels can probe
  // liveness without hard-coding a path.
  if (url.pathname === '/status' && (request.method === 'GET' || request.method === 'HEAD')) {
    return jsonResponse(
      {
        status: 'ok',
        service: 'slicc-tray-hub',
        timestamp: new Date().toISOString(),
      },
      200,
      { 'Cache-Control': 'no-store' }
    );
  }

  // Documentation pages for the SLICC custom rel URIs (per RFC 8288 §2.1.2,
  // extension rels SHOULD be dereferenceable). Match `/rel/<name>` only —
  // not `/rel/<name>/sub` — so we don't intercept future nested routes.
  const relMatch = url.pathname.match(/^\/rel\/([a-z0-9-]+)$/);
  if (relMatch && (request.method === 'GET' || request.method === 'HEAD')) {
    return buildRelResponse(relMatch[1]);
  }

  const tokenMatch = url.pathname.match(/^\/(join|controller|webhook)\/([^/]+?)(?:\/([^/]+))?$/);
  if (tokenMatch) {
    const route = tokenMatch[1];
    const token = tokenMatch[2];

    // Serve SPA for GET/HEAD browser navigation to join/controller URLs,
    // unless the client explicitly requests JSON via ?json=true
    // WebSocket upgrades must pass through to the Durable Object
    if (
      !wantsJSON(request) &&
      !request.headers.get('Upgrade') &&
      (route === 'join' || route === 'controller') &&
      (request.method === 'GET' || request.method === 'HEAD')
    ) {
      return serveSPA(request, env);
    }

    const parsed = parseCapabilityToken(token);
    if (!parsed) {
      return jsonResponse(
        { error: 'Malformed capability token', code: 'MALFORMED_CAPABILITY' },
        400
      );
    }
    const stub = env.TRAY_HUB.get(env.TRAY_HUB.idFromName(parsed.trayId));
    const webhookId = route === 'webhook' ? tokenMatch[3] : undefined;
    if (webhookId) {
      const doUrl = new URL(request.url);
      doUrl.pathname = `/webhook/${token}/${webhookId}`;
      return stub.fetch(new Request(doUrl, request));
    }
    return stub.fetch(request);
  }

  // SPA fallback for GET/HEAD browser navigation, unless ?json=true
  if (!wantsJSON(request) && (request.method === 'GET' || request.method === 'HEAD')) {
    return serveSPA(request, env);
  }

  return jsonResponse(
    {
      service: 'slicc-tray-hub',
      phase: 1,
      routes: [
        'POST /tray',
        'GET /download/slicc.dmg',
        'GET /handoff',
        'GET /.well-known/api-catalog',
        'GET /llms.txt',
        'GET /status',
        'GET /rel/:name',
        'GET|POST /join/:token',
        'GET|POST /controller/:token',
        'POST /webhook/:token/:webhookId',
        'GET /auth/callback',
        'POST /oauth/token',
        'POST /oauth/revoke',
        'GET /api/runtime-config',
        'ANY /api/fetch-proxy',
      ],
    },
    200
  );
}

const RELEASES_FALLBACK = 'https://github.com/ai-ecoverse/slicc/releases/latest';

async function handleDmgDownload(): Promise<Response> {
  let res: Response;
  try {
    res = await fetch(RELEASES_FALLBACK, { redirect: 'manual' });
  } catch {
    return Response.redirect(RELEASES_FALLBACK, 302);
  }
  const location = res.headers.get('Location');
  if (!location) {
    return Response.redirect(RELEASES_FALLBACK, 302);
  }
  // Location is like https://github.com/ai-ecoverse/slicc/releases/tag/v1.59.1
  const tag = location.split('/tag/')[1];
  if (!tag) {
    return Response.redirect(RELEASES_FALLBACK, 302);
  }
  // Strip leading 'v' for the filename: v1.59.1 → 1.59.1
  const version = tag.startsWith('v') ? tag.slice(1) : tag;
  const dmgUrl = `https://github.com/ai-ecoverse/slicc/releases/download/${tag}/sliccstart-v${version}.dmg`;
  return Response.redirect(dmgUrl, 302);
}

async function createTray(request: Request, env: WorkerEnv): Promise<Response> {
  const url = new URL(request.url);
  const trayId = crypto.randomUUID();
  const payload: CreateTrayRequest = {
    trayId,
    createdAt: new Date().toISOString(),
    joinToken: createCapabilityToken(trayId),
    controllerToken: createCapabilityToken(trayId),
    webhookToken: createCapabilityToken(trayId),
  };

  const stub = env.TRAY_HUB.get(env.TRAY_HUB.idFromName(trayId));
  const initResponse = await stub.fetch(
    new Request(new URL('/internal/create', url), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(payload),
    })
  );

  if (initResponse.status >= 400) {
    return initResponse;
  }

  return jsonResponse(
    {
      trayId,
      createdAt: payload.createdAt,
      capabilities: {
        join: {
          token: payload.joinToken,
          url: `${url.origin}/join/${payload.joinToken}`,
        },
        controller: {
          token: payload.controllerToken,
          url: `${url.origin}/controller/${payload.controllerToken}`,
        },
        webhook: {
          token: payload.webhookToken,
          url: `${url.origin}/webhook/${payload.webhookToken}`,
        },
      },
    },
    201
  );
}

const worker = {
  async fetch(request: Request, env: WorkerEnv): Promise<Response> {
    const url = new URL(request.url);

    // Root redirects to www.sliccy.com — indexable, return as-is
    if (url.pathname === '/' && url.search === '') {
      if (url.hostname === 'sliccy.ai') {
        return Response.redirect('https://www.sliccy.com/', 301);
      }
      if (url.hostname === 'www.sliccy.ai') {
        return Response.redirect('https://www.sliccy.com/', 301);
      }
    }

    const response = await handleWorkerRequest(request, env);
    if (response.status === 101) {
      return response;
    }
    // Apply SLICC's standard `Link` set, then attach the noindex tag.
    const withLinks = applySliccLinks(response, request);
    const mutable = new Response(withLinks.body, withLinks);
    mutable.headers.set('X-Robots-Tag', 'noindex');
    return mutable;
  },
};

export default worker;
export { SessionTrayDurableObject };
