/**
 * Cursor Daemon Entry
 *
 * Dedicated child-process entrypoint for local OpenAI-compatible Cursor proxy.
 */

import * as http from 'http';
import { Readable } from 'stream';
import { CursorExecutor } from './cursor-executor';
import {
  createAnthropicErrorResponse,
  createAnthropicProxyResponse,
} from './cursor-anthropic-response';
import { translateAnthropicRequest } from './cursor-anthropic-translator';
import { checkAuthStatus } from './cursor-auth';
import { getModelsForDaemon, resolveCursorRequestModel } from './cursor-models';
import type { CursorTool } from './cursor-protobuf-schema';

interface DaemonRuntimeOptions {
  port: number;
  ghostMode: boolean;
}

interface OpenAIMessage {
  role: string;
  content?: string | Array<{ type: string; text?: string }> | null;
  name?: string;
  tool_call_id?: string;
  tool_calls?: Array<{
    id: string;
    type: string;
    function: { name: string; arguments: string };
  }>;
}

interface NormalizedOpenAIMessage {
  role: string;
  content: string | Array<{ type: string; text?: string }>;
  name?: string;
  tool_call_id?: string;
  tool_calls?: Array<{
    id: string;
    type: string;
    function: { name: string; arguments: string };
  }>;
}

interface OpenAIChatRequest {
  model?: string;
  stream?: boolean;
  reasoning_effort?: string;
  tools?: CursorTool[];
  messages?: OpenAIMessage[];
}

const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB

function writeJson(res: http.ServerResponse, statusCode: number, payload: unknown): void {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(payload));
}

function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    let total = 0;
    let settled = false;

    const resolveOnce = (payload: unknown) => {
      if (settled) return;
      settled = true;
      resolve(payload);
    };

    const rejectOnce = (error: Error) => {
      if (settled) return;
      settled = true;
      reject(error);
    };

    req.on('data', (chunk: Buffer) => {
      total += chunk.length;
      if (total > MAX_BODY_SIZE) {
        // Stop processing body, but avoid force-closing socket so caller can return 413 cleanly.
        req.pause();
        rejectOnce(new Error('Request body too large (max 10MB)'));
        return;
      }
      chunks.push(chunk);
    });

    req.on('end', () => {
      const raw = Buffer.concat(chunks).toString('utf8').trim();
      if (!raw) {
        resolveOnce({});
        return;
      }
      try {
        resolveOnce(JSON.parse(raw));
      } catch {
        rejectOnce(new Error('Invalid JSON in request body'));
      }
    });

    req.on('error', (error) => {
      rejectOnce(error instanceof Error ? error : new Error(String(error)));
    });
  });
}

function normalizeMessages(raw: unknown): NormalizedOpenAIMessage[] {
  if (!Array.isArray(raw)) {
    throw new Error('messages must be an array');
  }

  return raw.map((message, index) => {
    if (typeof message !== 'object' || message === null) {
      throw new Error(`messages[${index}] must be an object`);
    }

    const m = message as Record<string, unknown>;
    if (typeof m.role !== 'string' || !m.role) {
      throw new Error(`messages[${index}].role must be a non-empty string`);
    }

    const content = m.content;
    if (
      content !== undefined &&
      content !== null &&
      typeof content !== 'string' &&
      !Array.isArray(content)
    ) {
      throw new Error(`messages[${index}].content must be string, array, or null`);
    }

    return {
      role: m.role,
      content: (content ?? '') as NormalizedOpenAIMessage['content'],
      name: typeof m.name === 'string' ? m.name : undefined,
      tool_call_id: typeof m.tool_call_id === 'string' ? m.tool_call_id : undefined,
      tool_calls: Array.isArray(m.tool_calls)
        ? (m.tool_calls as NormalizedOpenAIMessage['tool_calls'])
        : undefined,
    };
  });
}

async function pipeWebResponseToNode(response: Response, res: http.ServerResponse): Promise<void> {
  res.statusCode = response.status;
  response.headers.forEach((value, key) => {
    res.setHeader(key, value);
  });

  if (!response.body) {
    res.end();
    return;
  }

  const nodeStream = Readable.fromWeb(response.body as unknown as ReadableStream<Uint8Array>);

  await new Promise<void>((resolve, reject) => {
    nodeStream.on('error', reject);
    nodeStream.on('end', resolve);
    nodeStream.pipe(res);
  });
}

function parseArgs(argv: string[]): DaemonRuntimeOptions {
  let port = 20129;
  let ghostMode = true;

  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === '--port' && argv[i + 1]) {
      const parsed = parseInt(argv[i + 1], 10);
      if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
        port = parsed;
      }
      i++;
      continue;
    }

    if (arg === '--ghost-mode' && argv[i + 1]) {
      const value = argv[i + 1];
      ghostMode = value !== 'false' && value !== '0';
      i++;
      continue;
    }
  }

  return { port, ghostMode };
}

export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Server {
  const executor = new CursorExecutor();

  const server = http.createServer(async (req, res) => {
    const method = req.method || 'GET';
    const requestUrl = req.url || '/';
    const isOpenAiRoute = method === 'POST' && requestUrl === '/v1/chat/completions';
    const isAnthropicRoute = method === 'POST' && requestUrl === '/v1/messages';

    try {
      if (method === 'GET' && requestUrl === '/health') {
        writeJson(res, 200, { ok: true, service: 'cursor-daemon' });
        return;
      }

      if (method === 'GET' && requestUrl === '/v1/models') {
        const authStatus = checkAuthStatus();
        const models = await getModelsForDaemon({
          credentials:
            authStatus.authenticated && !authStatus.expired && authStatus.credentials
              ? {
                  accessToken: authStatus.credentials.accessToken,
                  machineId: authStatus.credentials.machineId,
                  ghostMode: options.ghostMode,
                }
              : null,
        });

        const data = models.map((model) => ({
          id: model.id,
          object: 'model',
          created: 0,
          owned_by: model.provider,
        }));
        writeJson(res, 200, { object: 'list', data });
        return;
      }

      if (!isOpenAiRoute && !isAnthropicRoute) {
        writeJson(res, 404, { error: 'Not found' });
        return;
      }

      const rawBody = await readJsonBody(req);
      const anthropicBody = isAnthropicRoute ? translateAnthropicRequest(rawBody) : undefined;
      const parsedBody = anthropicBody ?? ((rawBody as OpenAIChatRequest) || {});
      const messages = anthropicBody
        ? anthropicBody.messages
        : normalizeMessages(parsedBody.messages);
      const requestedModel =
        typeof parsedBody.model === 'string' && parsedBody.model.trim().length > 0
          ? parsedBody.model.trim()
          : undefined;
      const stream = parsedBody.stream === true;

      const authStatus = checkAuthStatus();
      if (!authStatus.authenticated || !authStatus.credentials) {
        const message = 'Cursor credentials not found. Run `ccs legacy cursor auth` first.';
        if (isAnthropicRoute) {
          await pipeWebResponseToNode(
            createAnthropicErrorResponse(401, 'authentication_error', message),
            res
          );
        } else {
          writeJson(res, 401, {
            error: {
              type: 'authentication_error',
              message,
            },
          });
        }
        return;
      }

      if (authStatus.expired) {
        const message = 'Cursor credentials expired. Run `ccs legacy cursor auth` again.';
        if (isAnthropicRoute) {
          await pipeWebResponseToNode(
            createAnthropicErrorResponse(401, 'authentication_error', message),
            res
          );
        } else {
          writeJson(res, 401, {
            error: {
              type: 'authentication_error',
              message,
            },
          });
        }
        return;
      }

      const daemonCredentials = {
        accessToken: authStatus.credentials.accessToken,
        machineId: authStatus.credentials.machineId,
        ghostMode: options.ghostMode,
      };
      const availableModels = await getModelsForDaemon({
        credentials: daemonCredentials,
      });
      const model = resolveCursorRequestModel(requestedModel, availableModels);
      if (
        requestedModel &&
        requestedModel !== model &&
        (process.env.CCS_DEBUG === '1' || process.env.CCS_DEBUG === 'true')
      ) {
        console.error(
          `[cursor] Requested model "${requestedModel}" is unavailable; falling back to "${model}".`
        );
      }

      const abortController = new AbortController();
      const abortOnDisconnect = () => {
        if (!abortController.signal.aborted && !res.writableEnded) {
          abortController.abort();
        }
      };

      req.on('aborted', abortOnDisconnect);
      req.on('close', abortOnDisconnect);
      res.on('close', abortOnDisconnect);

      const result = await executor.execute({
        model,
        stream,
        signal: abortController.signal,
        credentials: daemonCredentials,
        body: {
          messages,
          tools: Array.isArray(parsedBody.tools) ? parsedBody.tools : undefined,
          reasoning_effort:
            typeof parsedBody.reasoning_effort === 'string'
              ? parsedBody.reasoning_effort
              : undefined,
        },
      });

      const outgoingResponse = isAnthropicRoute
        ? await createAnthropicProxyResponse(result.response)
        : result.response;

      await pipeWebResponseToNode(outgoingResponse, res);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      const isPayloadTooLarge = message.includes('Request body too large');
      const status = isPayloadTooLarge ? 413 : 400;
      if (isAnthropicRoute) {
        await pipeWebResponseToNode(
          createAnthropicErrorResponse(status, 'invalid_request_error', message),
          res
        );
      } else {
        writeJson(res, status, {
          error: {
            type: 'invalid_request_error',
            message,
          },
        });
      }
    }
  });

  server.listen(options.port, '127.0.0.1');
  return server;
}

if (require.main === module) {
  const options = parseArgs(process.argv.slice(2));
  const server = startCursorDaemonServer(options);

  const shutdown = () => {
    server.close();
  };

  process.on('SIGTERM', shutdown);
  process.on('SIGINT', shutdown);
}
