#!/usr/bin/env node
import { writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { sections } from '../src/pages/api-docs/endpoints.ts';
import {
  buildWebSocketEvents,
  websocketEnvelopeSchema,
} from '../src/pages/api-docs/websocket-events.ts';
import { EXAMPLES } from '../src/generated/examples.ts';
import { SCHEMAS } from '../src/generated/schemas.ts';

const websocketEvents = buildWebSocketEvents(EXAMPLES);

const __dirname = dirname(fileURLToPath(import.meta.url));
const outPath = join(__dirname, '..', 'public', 'openapi.json');

const PANEL_VERSION = process.env.X_UI_VERSION || '3.x';

const SECURITY_SCHEMES = {
  bearerAuth: {
    type: 'http',
    scheme: 'bearer',
    description:
      'API token from Settings → Security → API Token. Send as `Authorization: Bearer <token>`.',
  },
  cookieAuth: {
    type: 'apiKey',
    in: 'cookie',
    name: '3x-ui',
    description: 'Session cookie set by POST /login. Browser-only.',
  },
};

function ginPathToOpenApi(path) {
  return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, '{$1}');
}

function extractPathParams(openApiPath) {
  const params = [];
  const re = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
  let m;
  while ((m = re.exec(openApiPath)) !== null) params.push(m[1]);
  return params;
}

function mapType(t) {
  const v = String(t || '').toLowerCase();
  if (v.endsWith('[]')) return 'array';
  if (v === 'number' || v === 'integer' || v === 'int') return 'integer';
  if (v === 'float' || v === 'double') return 'number';
  if (v === 'boolean' || v === 'bool') return 'boolean';
  if (v === 'array') return 'array';
  if (v === 'object') return 'object';
  return 'string';
}

function schemaFromType(t) {
  const v = String(t || '').toLowerCase();
  if (v.endsWith('[]')) {
    const itemType = v.slice(0, -2);
    return { type: 'array', items: { type: mapType(itemType) } };
  }
  if (v === 'file') return { type: 'string', format: 'binary' };
  return { type: mapType(v) };
}

function schemaFromParam(p) {
  const schema = schemaFromType(p.type);
  if (p.defaultValue !== undefined) schema.default = p.defaultValue;
  if (p.minLength !== undefined) schema.minLength = p.minLength;
  if (p.pattern !== undefined) schema.pattern = p.pattern;
  if (p.enum !== undefined) schema.enum = [...p.enum];
  return schema;
}

function requestBodyContentType(ep, bodyParams) {
  const locations = new Set(bodyParams.map((p) => p.in));
  if (locations.size > 1) {
    throw new Error(
      `${ep.method} ${ep.path}: request body mixes parameter locations: ${[...locations].join(', ')}`,
    );
  }
  switch (bodyParams[0]?.in) {
    case 'body (form)':
      return 'application/x-www-form-urlencoded';
    case 'body (multipart)':
      return 'multipart/form-data';
    default:
      return 'application/json';
  }
}

function tryParseJson(raw) {
  if (typeof raw !== 'string') return undefined;
  try {
    return JSON.parse(raw);
  } catch {
    return undefined;
  }
}

function paramToOpenApi(p) {
  const out = {
    name: p.name,
    in: p.in,
    required: p.in === 'path' ? true : !p.optional,
    description: p.desc || '',
    schema: schemaFromParam(p),
  };
  return out;
}

function buildOperation(ep, tag) {
  const op = {
    tags: [tag],
    summary: ep.summary || '',
    operationId: `${ep.method.toLowerCase()}_${ep.path.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_|_$/g, '')}`,
  };
  if (ep.description) op.description = ep.description;
  if (ep.deprecated) op.deprecated = true;

  const params = [];
  const bodyParams = [];
  for (const p of ep.params || []) {
    if (p.in.startsWith('body')) {
      bodyParams.push(p);
    } else if (p.in === 'path' || p.in === 'query' || p.in === 'header') {
      params.push(paramToOpenApi(p));
    }
  }

  const openApiPath = ginPathToOpenApi(ep.path);
  const declared = new Set(params.filter((x) => x.in === 'path').map((x) => x.name));
  for (const name of extractPathParams(openApiPath)) {
    if (declared.has(name)) continue;
    params.push({
      name,
      in: 'path',
      required: true,
      description: '',
      schema: { type: 'string' },
    });
  }

  if (params.length > 0) op.parameters = params;

  if (ep.body || bodyParams.length > 0 || ep.requestSchema) {
    const contentType = requestBodyContentType(ep, bodyParams);
    const example = contentType === 'application/json' ? tryParseJson(ep.body) : undefined;
    const properties = {};
    const required = [];
    for (const bp of bodyParams) {
      properties[bp.name] = {
        ...schemaFromParam(bp),
        description: bp.desc || '',
      };
      if (!bp.optional) required.push(bp.name);
    }
    let schema;
    if (ep.requestSchema) {
      if (bodyParams.length > 0 || ep.bodyRequiredOneOf?.length) {
        throw new Error(
          `${ep.method} ${ep.path}: requestSchema cannot be combined with body parameters or bodyRequiredOneOf`,
        );
      }
      schema = ep.requestSchema;
    } else {
      schema =
        bodyParams.length > 0
          ? { type: 'object', properties, ...(required.length > 0 ? { required } : {}) }
          : { type: 'object' };
      if (ep.bodyRequiredOneOf?.length) {
        schema = {
          anyOf: ep.bodyRequiredOneOf.map((name) => {
            if (!properties[name]) {
              throw new Error(
                `${ep.method} ${ep.path}: bodyRequiredOneOf "${name}" is not a declared body parameter`,
              );
            }
            const branchProperties = { ...properties };
            for (const other of ep.bodyRequiredOneOf) {
              if (other === name || !branchProperties[other]) continue;
              const { pattern: _pattern, minLength: _minLength, ...rest } = branchProperties[other];
              branchProperties[other] = rest;
            }
            return {
              type: 'object',
              properties: branchProperties,
              required: [...required, name],
            };
          }),
        };
      }
    }

    const encoding = {};
    if (contentType === 'application/x-www-form-urlencoded') {
      for (const bp of bodyParams) {
        const kind = schemaFromType(bp.type).type;
        if (kind === 'array') {
          encoding[bp.name] = { style: 'form', explode: true };
        } else if (kind === 'object') {
          // The panel reads such a field with json.Unmarshal, so it must be sent
          // as JSON text rather than form-style key/value pairs.
          encoding[bp.name] = { contentType: 'application/json' };
        }
      }
    }

    op.requestBody = {
      required:
        Boolean(ep.requestSchema) ||
        Boolean(ep.bodyRequiredOneOf?.length) ||
        required.length > 0 ||
        bodyParams.length === 0,
      content: {
        [contentType]: {
          schema,
          ...(Object.keys(encoding).length > 0 ? { encoding } : {}),
          ...(example !== undefined ? { example } : {}),
        },
      },
    };
  }

  const responses = {};
  let successExample = tryParseJson(ep.response);
  let objSchema = {};
  if (ep.responseObjectSchema && ep.responseSchema) {
    throw new Error(`${ep.method} ${ep.path}: responseObjectSchema cannot use responseSchema`);
  }
  if (ep.responseObjectSchema) objSchema = ep.responseObjectSchema;
  if (ep.responseSchema) {
    const obj = EXAMPLES[ep.responseSchema];
    if (obj === undefined) {
      throw new Error(
        `${ep.method} ${ep.path}: responseSchema "${ep.responseSchema}" has no generated example`,
      );
    }
    if (SCHEMAS[ep.responseSchema] === undefined) {
      throw new Error(
        `${ep.method} ${ep.path}: responseSchema "${ep.responseSchema}" has no generated schema`,
      );
    }
    const ref = { $ref: `#/components/schemas/${ep.responseSchema}` };
    objSchema = ep.responseSchemaArray ? { type: 'array', items: ref } : ref;
    if (successExample === undefined) {
      successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj };
    }
  }
  if (ep.responses) {
    Object.assign(responses, ep.responses);
  } else {
    responses['200'] = {
      description: 'Successful response',
      content: {
        'application/json': {
          schema: {
            type: 'object',
            properties: {
              success: { type: 'boolean' },
              msg: { type: 'string' },
              obj: objSchema,
            },
          },
          ...(successExample !== undefined ? { example: successExample } : {}),
        },
      },
    };
  }

  const errExample = tryParseJson(ep.errorResponse);
  if (errExample !== undefined || ep.errorStatus) {
    const code = String(ep.errorStatus || 400);
    responses[code] = {
      description: 'Error response',
      content: {
        'application/json': {
          schema: {
            type: 'object',
            properties: {
              success: { type: 'boolean' },
              msg: { type: 'string' },
            },
          },
          ...(errExample !== undefined ? { example: errExample } : {}),
        },
      },
    };
  }

  op.responses = responses;
  if (ep.security !== undefined) op.security = ep.security;
  return op;
}

export function buildSpec() {
  const paths = {};
  for (const section of sections) {
    const tag = section.title;
    for (const ep of section.endpoints) {
      const openApiPath = ginPathToOpenApi(ep.path);
      if (!paths[openApiPath]) paths[openApiPath] = {};
      paths[openApiPath][ep.method.toLowerCase()] = buildOperation(ep, tag);
    }
  }
  paths['/ws'].get['x-websocket-events'] = websocketEvents;

  const tags = sections.map((s) => ({
    name: s.title,
    description: s.description || '',
  }));

  return {
    openapi: '3.0.3',
    info: {
      title: '3X-UI Panel API',
      version: PANEL_VERSION,
      description:
        'Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with an API token from Settings → Security → API Token (Bearer). All endpoints under /panel/api/* honour both modes — an API token is a full-admin credential, so treat it like the panel password.',
    },
    servers: [{ url: '/', description: 'Current panel (basePath aware)' }],
    components: {
      securitySchemes: SECURITY_SCHEMES,
      schemas: { ...SCHEMAS, WebSocketEnvelope: websocketEnvelopeSchema },
    },
    security: [{ bearerAuth: [] }, { cookieAuth: [] }],
    tags,
    paths,
  };
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const spec = buildSpec();
  writeFileSync(outPath, JSON.stringify(spec, null, 2) + '\n');

  const pathCount = Object.keys(spec.paths).length;
  let opCount = 0;
  for (const ops of Object.values(spec.paths)) opCount += Object.keys(ops).length;
  console.log(`[openapi] wrote ${outPath}`);
  console.log(`[openapi] paths: ${pathCount}, operations: ${opCount}, tags: ${spec.tags.length}`);
}
