/**
 * check-openapi.js
 *
 * Validates the generated openapi.json against OpenAPI 3.0 spec
 * and checks orval-specific requirements (operationId, response schemas).
 *
 * Usage:
 *   pnpm swagger:check              # validate existing openapi.json
 *   pnpm swagger:generate && pnpm swagger:check  # full pipeline
 */

const SwaggerParser = require('@apidevtools/swagger-parser');
const { resolve } = require('path');
const { existsSync } = require('fs');

const SPEC_PATH = resolve(__dirname, '../storage/swagger/openapi.json');
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];

let exitCode = 0;

function fail(msg) {
	console.error(`❌ ${msg}`);
	exitCode = 1;
}

function warn(msg) {
	console.warn(`⚠️  ${msg}`);
}

function pass(msg) {
	console.log(`✅ ${msg}`);
}

async function main() {
	// ── 0. File existence ────────────────────────────────────────────────
	if (!existsSync(SPEC_PATH)) {
		fail(`openapi.json not found at ${SPEC_PATH}`);
		console.error('   Run `pnpm swagger:generate` first.');
		process.exit(1);
	}

	// ── 1. Structural validation ($ref resolution, schema validity) ──────
	let api;
	try {
		api = await SwaggerParser.validate(SPEC_PATH);
		pass('OpenAPI spec structure is valid (all $refs resolved)');
	} catch (err) {
		fail(`OpenAPI validation failed: ${err.message}`);
		process.exit(1);
	}

	// ── 2. Collect all operations ─────────────────────────────────────────
	const operations = [];
	for (const [path, pathItem] of Object.entries(api.paths || {})) {
		for (const method of HTTP_METHODS) {
			const op = pathItem[method];
			if (op) operations.push({ path, method: method.toUpperCase(), op });
		}
	}

	// ── 3. operationId — required by orval for function/hook naming ───────
	const operationIds = new Map(); // id → "METHOD /path"
	const missingOpId = [];
	const duplicateOpId = [];

	for (const { path, method, op } of operations) {
		const label = `${method} ${path}`;
		if (!op.operationId) {
			missingOpId.push(label);
		} else if (operationIds.has(op.operationId)) {
			duplicateOpId.push(`"${op.operationId}" at ${label} (first seen at ${operationIds.get(op.operationId)})`);
		} else {
			operationIds.set(op.operationId, label);
		}
	}

	if (missingOpId.length) {
		fail('Missing operationId — orval uses it to name generated hooks/functions:');
		missingOpId.forEach((op) => console.error(`   - ${op}`));
	} else {
		pass(`All ${operations.length} operations have operationId`);
	}

	if (duplicateOpId.length) {
		fail('Duplicate operationId — orval will overwrite generated code:');
		duplicateOpId.forEach((msg) => console.error(`   - ${msg}`));
	} else {
		pass('All operationIds are unique');
	}

	// ── 4. Response schema — orval generates `unknown` without it ─────────
	const missingResponseSchema = [];

	for (const { path, method, op } of operations) {
		const ok200 = op.responses?.['200'] || op.responses?.['201'];
		const content = ok200?.content || {};
		// Skip non-JSON responses (binary downloads, SSE streams)
		const hasNonJsonContent =
			content['application/octet-stream'] ||
			content['text/event-stream'] ||
			content['text/plain'] ||
			content['application/pdf'];
		if (hasNonJsonContent) continue;
		const hasSchema = content['application/json']?.schema;
		if (!hasSchema) {
			missingResponseSchema.push(`${method} ${path}`);
		}
	}

	if (missingResponseSchema.length) {
		warn('Missing 200/201 JSON response schema — orval will generate `unknown` for these:');
		missingResponseSchema.forEach((op) => console.warn(`   - ${op}`));
	}

	// ── 5. Tags — each operation should have at least one tag ─────────────
	const missingTags = [];
	for (const { path, method, op } of operations) {
		if (!op.tags || op.tags.length === 0) {
			missingTags.push(`${method} ${path}`);
		}
	}

	if (missingTags.length) {
		warn('Operations without tags (orval groups generated hooks by tag):');
		missingTags.forEach((op) => console.warn(`   - ${op}`));
	}

	// ── 6. Request body schema — orval needs typed body ──────────────────
	const missingBodySchema = [];
	for (const { path, method, op } of operations) {
		if (['POST', 'PUT', 'PATCH'].includes(method)) {
			const hasBodySchema = op.requestBody?.content?.['application/json']?.schema;
			const isMultipart = op.requestBody?.content?.['multipart/form-data'];
			if (!hasBodySchema && !isMultipart && op.requestBody) {
				missingBodySchema.push(`${method} ${path}`);
			}
		}
	}

	if (missingBodySchema.length) {
		warn('POST/PUT/PATCH missing request body schema (orval cannot generate typed payload):');
		missingBodySchema.forEach((op) => console.warn(`   - ${op}`));
	}

	// ── Summary ───────────────────────────────────────────────────────────
	console.log('');
	if (exitCode === 0) {
		pass(`OpenAPI check passed — ${operations.length} operations validated`);
		const warnings = missingResponseSchema.length + missingTags.length + missingBodySchema.length;
		if (warnings > 0) {
			console.log(`   (${warnings} warning(s) above — orval will work but some types may be incomplete)`);
		}
	} else {
		console.error('\n❌ OpenAPI check FAILED — fix errors above before running orval');
	}

	process.exit(exitCode);
}

main().catch((err) => {
	console.error('Unexpected error:', err);
	process.exit(1);
});
