#!/usr/bin/env node

import type {
	OpenAPIObject,
	SchemaObject,
	ReferenceObject,
} from '@bagelink/sdk'
import type {
	ArrayBagelField,
	ArrayFieldVal,
	BaseBagelField,
	BglFormSchemaT,
	Path,
	TextInputOptions,
} from '@bagelink/vue'
import type { InputTypeHTMLAttribute } from 'vue'
import { existsSync, mkdirSync } from 'node:fs'
import path from 'node:path'
import { argv, cwd, exit } from 'node:process'
import { isSchemaObject, isReferenceObject } from '@bagelink/sdk'
import { formatDate, useForm } from '@bagelink/vue'
import axios from 'axios'
import { toValue } from 'vue'
import {
	formatAndWriteCode,
	runEsLintOnDir,
	parseConfig,
	validateConfig,
	printHelp,
} from './utils'

// =============================================================================
// FORM UTILITIES (TOP-LEVEL SCOPE)
// =============================================================================

const {
	txtField,
	emailField,
	dateField,
	selectField,
	telField,
	numField,
	arrField,
	checkField,
} = useForm()

// =============================================================================
// TYPES & INTERFACES
// =============================================================================

interface FieldOptions {
	readonly helptext?: string
	readonly required: boolean
}

interface GeneratorConfig {
	readonly genSchemaId: string
	readonly output: string
	readonly url: string
	readonly useUtilityFunctions?: boolean
	readonly useTableSchema?: boolean
	readonly verbose?: boolean
}

interface SchemaContext {
	readonly openApiSchema: OpenAPIObject
}

type FormSchema<T> = BglFormSchemaT<T>
let USING_FMT_DATE = false
let USING_VERBOSE_FLAG = false

function logVerbose(...args: any[]): void {
	if (USING_VERBOSE_FLAG) {
		console.log('🔍', ...args)
	}
}

// =============================================================================
// CONFIGURATION
// =============================================================================

function parseConfiguration(): GeneratorConfig {
	const config = parseConfig({
		schemaId: undefined,
		defaultOutputFile: 'src/generated/exampleFormSchema.ts',
		allowedFlags: ['utility-functions', 'table-schema', 'help', 'verbose'],
	})

	if (argv.includes('--help') || argv.includes('-h')) {
		printHelp('generateFormSchema')
		exit(0)
	}

	try {
		validateConfig(config, ['baseUrl', 'schemaId'])
	} catch (error: any) {
		console.error('❌', error?.message)
		exit(1)
	}

	USING_VERBOSE_FLAG = config.verbose || false

	return {
		genSchemaId: config.schemaId || '',
		output: config.outputFile || 'src/generated/exampleFormSchema.ts',
		url: `${config.baseUrl}/openapi.json`,
		useUtilityFunctions: config.useUtilityFunctions,
		useTableSchema: config.useTableSchema,
	} as const
}

// =============================================================================
// SCHEMA UTILITIES
// =============================================================================

function resolveSchemaReference(
	schemaOrRef: SchemaObject | ReferenceObject,
	openApiSchema: OpenAPIObject,
	visitedRefs = new Set<string>()
): SchemaObject | undefined {
	if (!isReferenceObject(schemaOrRef)) {
		return isSchemaObject(schemaOrRef) ? schemaOrRef : undefined
	}

	const refPath = schemaOrRef.$ref
	if (visitedRefs.has(refPath)) {
		logVerbose(`Circular reference detected: ${refPath}`)
		return undefined
	}

	const refName = refPath.split('/').pop()
	if (!refName || !openApiSchema.components?.schemas?.[refName]) {
		logVerbose(`Could not resolve reference: ${refPath}`)
		return undefined
	}

	const resolvedSchema = openApiSchema.components.schemas[refName]
	if (isReferenceObject(resolvedSchema)) {
		visitedRefs.add(refPath)
		return resolveSchemaReference(resolvedSchema, openApiSchema, visitedRefs)
	}

	return isSchemaObject(resolvedSchema) ? resolvedSchema : undefined
}

function getSchemaById(
	schemaId: string,
	openApiSchema: OpenAPIObject
): SchemaObject | undefined {
	const schemaOrRef = openApiSchema.components?.schemas?.[schemaId]
	return schemaOrRef
		? resolveSchemaReference(schemaOrRef, openApiSchema)
		: undefined
}

/**
 * Resolves a schema reference and returns the actual schema object
 */
function resolveFieldSchema(
	fieldSchema: SchemaObject | ReferenceObject,
	openApiSchema: OpenAPIObject
): SchemaObject | undefined {
	if (isReferenceObject(fieldSchema)) {
		return resolveSchemaReference(fieldSchema, openApiSchema)
	}
	return isSchemaObject(fieldSchema) ? fieldSchema : undefined
}

/**
 * Gets the property schema for a nested field path
 */
function getNestedPropertySchema(
	fieldPath: string[],
	targetSchema: SchemaObject,
	openApiSchema: OpenAPIObject
): SchemaObject | undefined {
	if (fieldPath.length === 0) { return targetSchema }

	const [currentProp, ...remainingPath] = fieldPath
	const currentProperty = targetSchema.properties?.[currentProp]

	if (!currentProperty) {
		logVerbose(`Property '${currentProp}' not found in schema`)
		return undefined
	}

	const resolvedProperty = resolveFieldSchema(
		currentProperty,
		openApiSchema
	)
	if (!resolvedProperty) {
		logVerbose(`Could not resolve property '${currentProp}'`)
		return undefined
	}

	// If there are more path segments, continue resolving
	if (remainingPath.length > 0) {
		if (resolvedProperty.type === 'object' && resolvedProperty.properties) {
			return getNestedPropertySchema(
				remainingPath,
				resolvedProperty,
				openApiSchema
			)
		}
		logVerbose(
			`Cannot resolve nested path beyond '${currentProp}' - not an object type`
		)
		return undefined
	}

	return resolvedProperty
}

// =============================================================================
// STRING FORMATTING
// =============================================================================

function formatPropertyName(name: string): string {
	return name
		.replace(/([A-Z])/g, ' $1')
		.replace(/_/g, ' ')
		.replace(/^\w/, c => c.toUpperCase())
		.trim()
}

function formatTypeName(name: string): string {
	return name
		.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase())
		.replace(/^\w/, c => c.toUpperCase())
		.replace(/\s+(\w)/g, (_, letter: string) => letter.toUpperCase())
}

function getHtmlInputType(format?: string): string | undefined {
	const formatMap: Record<string, string> = {
		'date': 'date',
		'date-time': 'datetime-local',
		'email': 'email',
		'phone': 'tel',
		'password': 'password',
		'binary': 'file',
		'uri': 'url',
		'double': 'number',
		'float': 'number',
		'int32': 'number',
		'int64': 'number',
	}

	return format ? formatMap[format] : undefined
}

// =============================================================================
// FIELD CREATION
// =============================================================================

function createStringField<T>(
	propName: Path<T>,
	schema: SchemaObject,
	options: FieldOptions
): BaseBagelField<T, Path<T>> {
	const label = formatPropertyName(propName as string)

	// Email fields
	if (schema.format === 'email') {
		return emailField(propName, label, {
			required: options.required,
			placeholder: options.helptext,
		})
	}

	// Date fields
	if (schema.format === 'date' || schema.format === 'date-time') {
		return dateField(propName, label, {
			required: options.required,
			placeholder: options.helptext,
			enableTime: schema.format === 'date-time',
		})
	}

	// Phone fields
	if (schema.format === 'phone') {
		return telField(propName, label, {
			required: options.required,
			placeholder: options.helptext,
		})
	}

	// Enum fields as select
	if (schema.enum?.length) {
		const selectOptions = schema.enum.map(value => ({
			label: String(value)
				.replace(/_/g, ' ')
				.replace(/\b\w/g, l => l.toUpperCase()),
			value,
		}))
		return selectField(propName, label, selectOptions, {
			required: options.required,
			placeholder: options.helptext,
		})
	}

	// Default text field
	const textOptions: TextInputOptions<T, Path<T>> = {
		required: options.required,
		helptext: options.helptext,
		multiline: (schema.maxLength ?? 0) > 255,
		pattern: schema.pattern,
		type: getHtmlInputType(schema.format) as InputTypeHTMLAttribute,
	}

	return txtField(propName, label, textOptions)
}

function createNumberField<T>(
	propName: Path<T>,
	schema: SchemaObject,
	options: FieldOptions
): BaseBagelField<T, Path<T>> {
	return numField(
		propName,
		formatPropertyName(propName as string),
		{
			required: options.required,
			placeholder: options.helptext,
			min: schema.minimum,
			max: schema.maximum,
			step: schema.type === 'integer' ? 1 : undefined,
		}
	)
}

function createBooleanField<T>(
	propName: Path<T>,
	options: FieldOptions
): BaseBagelField<T, Path<T>> {
	return checkField(propName, formatPropertyName(propName as string), {
		required: options.required,
	})
}

function createArrayField<T>(
	propName: Path<T>,
	schema: SchemaObject,
	options: FieldOptions,
	schemaType: string,
	context: SchemaContext
): BaseBagelField<T, Path<T>> {
	if (!schema.items) {
		logVerbose(`Array field '${propName as string}' has no items schema`)
		return createFallbackField(propName, options)
	}

	const itemsSchema = resolveSchemaReference(
		schema.items,
		context.openApiSchema
	)
	if (!itemsSchema) {
		logVerbose(
			`Array field '${propName as string}' has unresolvable items schema`
		)
		return createFallbackField(propName, options)
	}

	// Object array
	if (itemsSchema.type === 'object' && itemsSchema.properties) {
		const nestedSchema = convertSchemaToForm(
			itemsSchema,
			`${schemaType}Item`,
			context
		) as FormSchema<ArrayFieldVal<T, Path<T>>>

		return arrField(
			propName,
			formatPropertyName(propName as string),
			nestedSchema
		)
	}

	// Primitive array
	const arrayType
		= itemsSchema.type === 'number' || itemsSchema.type === 'integer'
			? 'number'
			: 'text'
	return arrField(
		propName,
		formatPropertyName(propName as string),
		arrayType
	)
}

function createFallbackField<T>(
	propName: Path<T>,
	options: FieldOptions
): BaseBagelField<T, Path<T>> {
	return txtField(
		propName,
		formatPropertyName(propName as string),
		{
			required: options.required,
			placeholder: options.helptext,
		}
	)
}

function createFieldByType<T>(
	propName: Path<T>,
	schema: SchemaObject,
	options: FieldOptions,
	schemaType: string,
	context: SchemaContext
): BaseBagelField<T, Path<T>> | null {
	// Handle nullable union types
	const actualSchema = getNonNullSchema(schema) || schema

	switch (actualSchema.type) {
		case 'string':
			return createStringField(propName, actualSchema, options)
		case 'number':
		case 'integer':
			return createNumberField(propName, actualSchema, options)
		case 'boolean':
			return createBooleanField(propName, options)
		case 'array':
			return createArrayField(
				propName,
				actualSchema,
				options,
				schemaType,
				context
			)
		case 'object':
			return null // Will be handled separately for flattening
		default:
			logVerbose(
				`Unknown type '${actualSchema.type}' for field '${propName as string}', using fallback`
			)
			return createFallbackField(propName, options)
	}
}

function getNonNullSchema(schema: SchemaObject): SchemaObject | undefined {
	// Handle anyOf union types
	if (schema.anyOf && Array.isArray(schema.anyOf)) {
		const nonNullSchema = schema.anyOf.find((subSchema) => {
			if (isReferenceObject(subSchema)) {
				// We'd need to resolve this, but for simplicity, assume it's not null
				return true
			}
			return subSchema.type !== 'null'
		})

		return nonNullSchema && !isReferenceObject(nonNullSchema)
			? nonNullSchema
			: undefined
	}

	// Handle oneOf union types
	if (schema.oneOf && Array.isArray(schema.oneOf)) {
		const nonNullSchema = schema.oneOf.find((subSchema) => {
			if (isReferenceObject(subSchema)) {
				return true
			}
			return subSchema.type !== 'null'
		})

		return nonNullSchema && !isReferenceObject(nonNullSchema)
			? nonNullSchema
			: undefined
	}

	return undefined
}

function processObjectField<T>(
	propName: Path<T>,
	schema: SchemaObject,
	schemaType: string,
	formSchema: FormSchema<T>,
	context: SchemaContext
): void {
	if (!schema.properties || Object.keys(schema.properties).length === 0) { return }

	logVerbose(`Creating nested fields for object: ${propName as string}`)
	const nestedSchema = convertSchemaToForm(
		schema,
		`${schemaType}${formatTypeName(propName as string)}`,
		context
	)

	// Flatten nested fields with proper namespacing
	nestedSchema.forEach((subField) => {
		if (!subField.id) { return }

		const nestedField = {
			...subField,
			id: `${propName as string}.${subField.id}` as Path<T>,
		}
		formSchema.push(nestedField as FormSchema<T>[number])
		logVerbose(`Added nested field: ${propName as string}.${subField.id}`)
	})
}

// =============================================================================
// SCHEMA CONVERSION
// =============================================================================

function convertSchemaToForm<T = Record<string, unknown>>(
	schema: SchemaObject,
	schemaType: string,
	context: SchemaContext
): FormSchema<T> {
	const propertyKeys = Object.keys(schema.properties ?? {})
	logVerbose(`Processing schema for ${schemaType}:`, propertyKeys)

	if (!schema.properties) {
		logVerbose('Schema has no properties')
		return []
	}

	const formSchema: FormSchema<T> = []
	const requiredFields = Array.isArray(schema.required) ? schema.required : []

	Object.entries(schema.properties).forEach(([propName, propSchemaOrRef]) => {
		const resolvedPropSchema = resolveSchemaReference(
			propSchemaOrRef,
			context.openApiSchema
		)
		if (!resolvedPropSchema) {
			logVerbose(`Skipping unresolvable property: ${propName}`)
			return
		}

		const fieldOptions: FieldOptions = {
			required: requiredFields.includes(propName),
			helptext: resolvedPropSchema.description,
		}

		logVerbose(
			`Processing field: ${propName}, type: ${resolvedPropSchema.type ?? 'undefined'}, format: ${resolvedPropSchema.format ?? 'undefined'}`
		)

		// Handle object fields by flattening
		if (resolvedPropSchema.type === 'object') {
			processObjectField(
				propName as Path<T>,
				resolvedPropSchema,
				schemaType,
				formSchema,
				context
			)
			return
		}

		const field = createFieldByType(
			propName as Path<T>,
			resolvedPropSchema,
			fieldOptions,
			schemaType,
			context
		)
		if (!field) { return }

		// Apply default value if specified
		if (resolvedPropSchema.default !== undefined) {
			field.defaultValue = resolvedPropSchema.default
		}

		formSchema.push(field as FormSchema<T>[number])
		logVerbose(`Added field: ${propName} with $el: ${field.$el}`)
	})

	logVerbose(`Generated ${formSchema.length} fields for ${schemaType}`)
	return formSchema
}

async function fetchOpenApiSchema(url: string): Promise<OpenAPIObject> {
	try {
		const basicAuthHeader = {
			Authorization: 'Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs',
		}
		const { data } = await axios.get<OpenAPIObject>(url, {
			headers: basicAuthHeader,
			timeout: 10000,
		})
		return data
	} catch (error) {
		console.error('🚨 Failed to fetch OpenAPI schema:', error)
		const message = error instanceof Error ? error.message : 'Unknown error'
		throw new Error(`Failed to fetch OpenAPI schema: ${message}`)
	}
}

// =============================================================================
// TABLE SCHEMA CONVERSION
// =============================================================================

function addTransformForType(
	field: BaseBagelField<any, any>,
	schemaType: string,
	format?: string
): void {
	switch (schemaType) {
		case 'boolean':
			field.transform = (value: any) => {
				if (value === true) { return 'Yes' }
				if (value === false) { return 'No' }
				return value
			}
			field.class = (value: any) => `pill ${value === true ? 'green' : 'light'}`
			break

		case 'string':
			if (format === 'date' || format === 'date-time') {
				USING_FMT_DATE = true
				field.transform = (value: any) => formatDate(value, 'YYYY-MM-DD')
			} else if (format === 'email') {
				field.attrs = { ...field.attrs, dir: 'ltr' }
			} else if (format === 'phone') {
				field.attrs = { ...field.attrs, dir: 'ltr' }
			} else if (format === 'uri' || format === 'url') {
				field.attrs = { ...field.attrs, dir: 'ltr' }
			}
			break

		case 'number':
		case 'integer':
			field.transform = (value: any) => Number.isNaN(value) ? '' : value?.toLocaleString()
			field.attrs = { ...field.attrs, dir: 'ltr' }
			break

		case 'array':
			field.transform = (value: any) => {
				if (!Array.isArray(value) || value.length === 0) { return '' }
				if (typeof value[0] === 'object') { return `${value.length} items` }
				return value.join(', ')
			}
			field.attrs = { ...field.attrs, dir: 'ltr' }
			break

		case 'object':
			field.transform = (value: any) => {
				if (!value || typeof value !== 'object') { return '' }
				return 'Object'
			}
			break

		default:
			// Default string handling
			break
	}
}

/**
 * Adds fallback transform based on field element type
 */
function addFallbackTransform<T>(
	tableField: BaseBagelField<T, Path<T>>,
	fieldElementType: string | undefined
): void {
	switch (fieldElementType) {
		case 'date':
			USING_FMT_DATE = true
			tableField.transform = (value: any) => formatDate(value, 'YYYY-MM-DD')
			break

		case 'check':
			tableField.transform = (value: unknown) => value === true ? 'Yes' : value === false ? 'No' : value
			tableField.class = (value: unknown) => `pill ${value === true ? 'green' : 'light'}`
			break

		case 'email':
			tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
			break

		case 'tel':
			tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
			break

		case 'number':
			tableField.transform = (value: any) => {
				if (value === null || value === undefined) { return '' }
				return Number(value).toLocaleString()
			}
			tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
	}
}

function processFieldForTable<T>(
	field: BaseBagelField<T, Path<T>>,
	targetSchema: SchemaObject,
	openApiSchema: OpenAPIObject
): BaseBagelField<T, Path<T>> | undefined {
	if (!field.id) { return undefined }

	const fieldPath = field.id.toString().split('.')
	const resolvedFieldSchema = getNestedPropertySchema(
		fieldPath,
		targetSchema,
		openApiSchema
	)

	if (!resolvedFieldSchema) {
		logVerbose(`No schema found for field: ${field.id}`)
		return undefined
	}

	const tableField: BaseBagelField<T, Path<T>> = {
		id: field.id,
		label: field.label,
		attrs: {},
	}

	// Handle union types (anyOf) - extract the non-null type
	const actualSchema
		= getNonNullSchema(resolvedFieldSchema) || resolvedFieldSchema

	// Safely extract the schema type as a string
	const schemaType = getSchemaTypeAsString(actualSchema.type)
	addTransformForType(tableField, schemaType, actualSchema.format)

	// Handle fallback based on original field element type
	if (!tableField.transform) {
		addFallbackTransform(tableField, field.$el)
	}

	return tableField
}

/**
 * Safely converts schema type to string, handling arrays and undefined values
 */
function getSchemaTypeAsString(type: SchemaObject['type']): string {
	if (Array.isArray(type)) {
		// For arrays of types, prefer non-null types or return the first one
		const nonNullType = type.find(t => t !== 'null' && t !== 'undefined')
		return nonNullType || type[0] || 'string'
	}
	return type || 'string'
}

function convertToTableSchema<T = Record<string, unknown>>(
	formSchema: FormSchema<T>,
	targetSchema: SchemaObject,
	openApiSchema: OpenAPIObject
): FormSchema<T> {
	const tableSchema: any[] = []

	formSchema.forEach((field) => {
		const processedField = processFieldForTable(
			field as unknown as BaseBagelField<any, Path<any>>,
			targetSchema,
			openApiSchema
		)
		if (processedField) {
			(tableSchema as any[]).push(processedField as any)
		}
	})

	logVerbose(
		`Converted ${formSchema.length} form fields to ${tableSchema.length} table fields`
	)
	return tableSchema as FormSchema<T>
}

// =============================================================================
// CODE GENERATION
// =============================================================================

function cleanOptions(options: Record<string, any>): string {
	if (!options || typeof options !== 'object') { return '{}' }

	const cleaned: Record<string, any> = {}
	const skipDefaults = ['step', 'enableTime', 'multiline', 'required']

	Object.entries(options).forEach(([key, value]) => {
		const shouldSkip
			= value === undefined
				|| value === null
				|| value === false
				|| value === ''
				|| value === 0
				|| (skipDefaults.includes(key) && (value === 1 || value === false))
				|| (key === 'attrs'
					&& typeof value === 'object'
					&& Object.keys(value).length === 0)

		if (!shouldSkip) {
			cleaned[key] = value
		}
	})

	return Object.keys(cleaned).length > 0 ? JSON.stringify(cleaned) : '{}'
}

function extractFieldOptions<T>(field: BaseBagelField<T, Path<T>>) {
	const fieldProps = [
		'required',
		'placeholder',
		'helptext',
		'defaultValue',
		'class',
		'disabled',
		'vIf',
		'transform',
		'onUpdate',
		'validate',
	] as const

	type OptFieldProps = typeof fieldProps[number] | (string & {})
	const options: Partial<Record<OptFieldProps, any>> = {}

	fieldProps.forEach((prop) => {
		if (field[prop] !== undefined) {
			options[prop] = field[prop]
		}
	})

	if (field.attrs && typeof field.attrs === 'object') {
		Object.entries(field.attrs).forEach(([key, value]) => {
			if (value !== undefined && value !== null) {
				options[key] = value
			}
		})
	}

	return options
}

function generateFieldCall<T>(field: any, useTableSchema: boolean): string {
	const fieldId = field.id as string
	const fieldLabel = field.label || formatPropertyName(fieldId)
	const fieldOptions = extractFieldOptions(field)
	const cleanedOptions = cleanOptions(fieldOptions)

	if (useTableSchema) {
		const options: string[] = []
		const specialProps = ['transform', 'class', 'attrs'] as const

		specialProps.forEach((prop) => {
			if (field[prop]) {
				const value = prop === 'attrs'
					? JSON.stringify(field[prop])
					: field[prop].toString()
				options.push(`${prop}: ${value}`)
			}
		})

		const optionsStr = options.length > 0 ? `{ ${options.join(', ')} }` : ''
		return `getBaseField('${fieldId}', '${fieldLabel}'${optionsStr ? `, ${optionsStr}` : ''})`
	}

	const fieldTypeMap: Record<string, string> = {
		text: 'txtField',
		email: 'emailField',
		date: 'dateField',
		tel: 'telField',
		number: 'numField',
		check: 'checkField',
		checkbox: 'checkField',
	}

	const fieldType = fieldTypeMap[field.$el] || 'txtField'

	if (field.$el === 'select') {
		const selectOptions
			= field.options || (field.attrs && field.attrs.options) || []
		const optionsStr = JSON.stringify(selectOptions)
		return `selectField('${fieldId}', '${fieldLabel}', ${optionsStr}, ${cleanedOptions})`
	}

	if (field.$el === 'array') {
		const arrayField = field as ArrayBagelField<any, any>
		if (arrayField.attrs?.schema) {
			const nestedCalls = toValue(arrayField.attrs.schema).map(
				(nestedField: any) => generateFieldCall(nestedField, useTableSchema)
			)
			const nestedSchemaStr = `[${nestedCalls.join(',')}]`
			return `arrField('${fieldId}', '${fieldLabel}', ${nestedSchemaStr})`
		}
		const arrayType = arrayField.attrs?.type || 'text'
		return `arrField('${fieldId}', '${fieldLabel}', '${arrayType}')`
	}

	return `${fieldType}('${fieldId}', '${fieldLabel}', ${cleanedOptions})`
}

function generateCodeWithUtilities<T>(
	schemaId: string,
	formSchema: FormSchema<T>,
	config: GeneratorConfig
): string {
	const imports = new Set<string>()
	const fieldCalls: string[] = []

	formSchema.forEach((field) => {
		const call = generateFieldCall(field as any, config.useTableSchema || false)
		fieldCalls.push(call)

		// Extract import from function call
		const functionName = call.split('(')[0]
		imports.add(functionName)
	})

	if (config.useTableSchema) {
		imports.add('getBaseField')
	}

	const importList = Array.from(imports).sort().join(', ')
	const importStatement
		= imports.size > 0 ? `const { ${importList} } = useForm()` : ''

	return `import type { BglFormSchemaT } from '@bagelink/vue'
import type { ${schemaId} } from '@/client/types'
import { useForm ${USING_FMT_DATE ? ', formatDate' : ''} } from '@bagelink/vue'

${importStatement}

/**
 * Generated form schema for ${schemaId}
 */
export function generate${schemaId}FormSchema(): BglFormSchemaT<${schemaId}> {
    return [${fieldCalls.join(',\n\t\t')}]
}`
		.replaceAll(`value === !0`, 'value === true')
		.replaceAll(`value === !1`, 'value === false')
		.replaceAll(`attrs: {}`, '')
		.replaceAll('{}', '')
		.replaceAll('{  }', '')
}

function generateCodeWithJson(
	schemaId: string,
	formSchema: FormSchema<any>
): string {
	const stringifiedSchema = JSON.stringify(formSchema)
		.replaceAll(`"step":1`, '')
		.replaceAll(`"enableTime":false`, '')
		.replaceAll(`"multiline":false`, '')
		.replaceAll(`"required":false`, '')
		.replaceAll(`"attrs":{}`, '')
		.replaceAll(`,,`, ',')

	return `import type { BglFormSchemaT } from '@bagelink/vue'
import type { ${schemaId} } from '@/client/types'

/**
 * Generated form schema for ${schemaId}
 */
export function generate${schemaId}FormSchema(): BglFormSchemaT<${schemaId}> {
    return ${stringifiedSchema}
}
`
}

function generateCode(
	schemaId: string,
	formSchema: FormSchema<any>,
	config: GeneratorConfig
): string {
	return config.useUtilityFunctions
		? generateCodeWithUtilities(schemaId, formSchema, config)
		: generateCodeWithJson(schemaId, formSchema)
}

// =============================================================================
// FILE OPERATIONS
// =============================================================================

async function writeGeneratedCode(
	outputFile: string,
	content: string
): Promise<void> {
	const outputDir = path.dirname(outputFile)
	if (!existsSync(outputDir)) {
		mkdirSync(outputDir, { recursive: true })
	}

	const fullOutputPath = path.resolve(cwd(), outputFile)
	await formatAndWriteCode(fullOutputPath, content)
	await runEsLintOnDir(outputDir)

	console.log(`📁 Output file location: ${fullOutputPath}`)
}

// =============================================================================
// MAIN EXECUTION
// =============================================================================

async function main(): Promise<void> {
	try {
		console.log('⚙️  Parsing configuration...')
		const config = parseConfiguration()

		console.log(`🌐 Fetching OpenAPI schema from ${config.url}...`)
		const openApiSchema = await fetchOpenApiSchema(config.url)

		if (!openApiSchema.components?.schemas) {
			throw new Error('OpenAPI schema has no components.schemas section')
		}

		console.log(`🔍 Finding schema: ${config.genSchemaId}`)
		const targetSchema = getSchemaById(config.genSchemaId, openApiSchema)
		if (!targetSchema) {
			throw new Error(
				`Schema with ID "${config.genSchemaId}" not found or is not resolvable`
			)
		}

		const context: SchemaContext = { openApiSchema }

		console.log(`🔄 Converting schema to form fields...`)
		let formSchema = convertSchemaToForm(targetSchema, config.genSchemaId, context)

		if (config.useTableSchema) {
			console.log('📊 Converting form schema to table schema...')
			formSchema = convertToTableSchema(formSchema, targetSchema, openApiSchema)
		}

		console.log('🧑‍💻 Generating TypeScript code...')
		const codeContent = generateCode(config.genSchemaId, formSchema, config)

		logVerbose(`Generated code for schema "${config.genSchemaId}":\n${codeContent}`)

		console.log(`💾 Writing output to ${config.output}...`)
		await writeGeneratedCode(config.output, codeContent)

		console.log('✅ Form schema generation completed successfully!')
	} catch (error) {
		console.error('❌ Error generating form schema:', error)
		exit(1)
	}
}

// Execute the main pipeline
main().catch(console.error)
