/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { existsSync, readFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { basename, dirname } from 'node:path/posix';
import { GENERATOR, type State, generate } from 'astring';
import type * as estree from 'estree';
import glob from 'fast-glob';
import { format } from 'prettier';

interface SatisfiesExpression extends estree.BaseExpression {
	type: 'SatisfiesExpression';
	expression: estree.Expression;
	reference: estree.Identifier;
}

const generator = {
	...GENERATOR,
	SatisfiesExpression(node: SatisfiesExpression, state: State) {
		switch (node.expression.type) {
			case 'ArrowFunctionExpression': {
				state.write('(');
				this[node.expression.type](node.expression, state);
				state.write(')');
				break;
			}
			default: {
				// @ts-ignore
				this[node.expression.type](node.expression, state);
				break;
			}
		}
		state.write(' satisfies ', node as unknown as estree.Expression);
		this[node.reference.type](node.reference, state);
	},
};

type SplitCamel<
	T extends string,
	YC extends string = '',
	YN extends readonly string[] = []
> = T extends `${infer XH}${infer XR}`
	? XR extends ''
		? [...YN, Uncapitalize<`${YC}${XH}`>]
		: XH extends Uppercase<XH>
		? SplitCamel<XR, Lowercase<XH>, [...YN, YC]>
		: SplitCamel<XR, `${YC}${XH}`, YN>
	: YN;

// @ts-ignore
type SplitKebab<T extends string> = T extends `${infer XH}-${infer XR}`
	? [XH, ...SplitKebab<XR>]
	: [T];

type ToKebab<T extends readonly string[]> = T extends readonly [
	infer XO extends string
]
	? XO
	: T extends readonly [
			infer XH extends string,
			...infer XR extends readonly string[]
	  ]
	? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
	: '';

// @ts-ignore
type ToPascal<T extends readonly string[]> = T extends readonly [
	infer XH extends string,
	...infer XR extends readonly string[]
]
	? `${Capitalize<XH>}${ToPascal<XR>}`
	: '';

function h<T extends estree.Node>(
	component: T['type'],
	props: Omit<T, 'type'>
): T {
	const type = component.replace(/(?:^|-)([a-z])/g, (_, c) => c.toUpperCase());
	return Object.assign(props || {}, { type }) as T;
}

declare namespace h.JSX {
	type Element = estree.Node;
	type IntrinsicElements = {
		[T in keyof typeof generator as ToKebab<SplitCamel<Uncapitalize<T>>>]: {
			[K in keyof Omit<
				Parameters<(typeof generator)[T]>[0],
				'type'
			>]?: Parameters<(typeof generator)[T]>[0][K];
		};
	};
}

function toStories(component: string): Promise<string> {
	const msw = `${component.slice(0, -'.vue'.length)}.msw`;
	const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`;
	const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`;
	const hasMsw = existsSync(`${msw}.ts`);
	const hasImplStories = existsSync(`${implStories}.ts`);
	const hasMetaStories = existsSync(`${metaStories}.ts`);
	const base = basename(component);
	const dir = dirname(component);
	const literal =
		<literal
			value={component
				.slice('src/'.length, -'.vue'.length)
				.replace(/\./g, '/')}
		/> as estree.Literal;
	const identifier =
		<identifier
			name={base
				.slice(0, -'.vue'.length)
				.replace(/[-.]|^(?=\d)/g, '_')
				.replace(/(?<=^[^A-Z_]*$)/, '_')}
		/> as estree.Identifier;
	const parameters =
		<object-expression
			properties={[
				<property
					key={<identifier name='layout' /> as estree.Identifier}
					value={<literal value={`${dir}/`.startsWith('src/pages/') ? 'fullscreen' : 'centered'}/> as estree.Literal}
					kind={'init' as const}
				/> as estree.Property,
				...(hasMsw
					? [
							<property
								key={<identifier name='msw' /> as estree.Identifier}
								value={<identifier name='msw' /> as estree.Identifier}
								kind={'init' as const}
								shorthand
							/> as estree.Property,
					  ]
					: []),
			]}
		/> as estree.ObjectExpression;
	const program =
		<program
			body={[
				<import-declaration
					source={<literal value='@storybook/vue3' /> as estree.Literal}
					specifiers={[
						<import-specifier
							local={<identifier name='Meta' /> as estree.Identifier}
							imported={<identifier name='Meta' /> as estree.Identifier}
						/> as estree.ImportSpecifier,
						...(hasImplStories
							? []
							: [
									<import-specifier
										local={<identifier name='StoryObj' /> as estree.Identifier}
										imported={<identifier name='StoryObj' /> as estree.Identifier}
									/> as estree.ImportSpecifier,
								]),
					]}
				/> as estree.ImportDeclaration,
				...(hasMsw
					? [
							<import-declaration
								source={<literal value={`./${basename(msw)}`} /> as estree.Literal}
								specifiers={[
									<import-namespace-specifier
										local={<identifier name='msw' /> as estree.Identifier}
									/> as estree.ImportNamespaceSpecifier,
								]}
							/> as estree.ImportDeclaration,
					  ]
					: []),
				...(hasImplStories
					? []
					: [
							<import-declaration
								source={<literal value={`./${base}`} /> as estree.Literal}
								specifiers={[
									<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
								]}
							/> as estree.ImportDeclaration,
					  ]),
				...(hasMetaStories
					? [
							<import-declaration
								source={<literal value={`./${basename(metaStories)}`} /> as estree.Literal}
								specifiers={[
									<import-namespace-specifier
										local={<identifier name='storiesMeta' /> as estree.Identifier}
									/> as estree.ImportNamespaceSpecifier,
								]}
							/> as estree.ImportDeclaration,
						]
					: []),
				<variable-declaration
					kind={'const' as const}
					declarations={[
						<variable-declarator
							id={<identifier name='meta' /> as estree.Identifier}
							init={
								<satisfies-expression
									expression={
										<object-expression
											properties={[
												<property
													key={<identifier name='title' /> as estree.Identifier}
													value={literal}
													kind={'init' as const}
												/> as estree.Property,
												<property
													key={<identifier name='component' /> as estree.Identifier}
													value={identifier}
													kind={'init' as const}
												/> as estree.Property,
												...(hasMetaStories
													? [
															<spread-element
																argument={<identifier name='storiesMeta' /> as estree.Identifier}
															/> as estree.SpreadElement,
														]
													: [])
											]}
										/> as estree.ObjectExpression
									}
									reference={<identifier name={`Meta<typeof ${identifier.name}>`} /> as estree.Identifier}
								/> as estree.Expression
							}
						/> as estree.VariableDeclarator,
					]}
				/> as estree.VariableDeclaration,
				...(hasImplStories
					? []
					: [
							<export-named-declaration
								declaration={
									<variable-declaration
										kind={'const' as const}
										declarations={[
											<variable-declarator
												id={<identifier name='Default' /> as estree.Identifier}
												init={
													<satisfies-expression
														expression={
															<object-expression
																properties={[
																	<property
																		key={<identifier name='render' /> as estree.Identifier}
																		value={
																			<function-expression
																				params={[
																					<identifier name='args' /> as estree.Identifier,
																				]}
																				body={
																					<block-statement
																						body={[
																							<return-statement
																								argument={
																									<object-expression
																										properties={[
																											<property
																												key={<identifier name='components' /> as estree.Identifier}
																												value={
																													<object-expression
																														properties={[
																															<property key={identifier} value={identifier} kind={'init' as const} shorthand /> as estree.Property,
																														]}
																													/> as estree.ObjectExpression
																												}
																												kind={'init' as const}
																											/> as estree.Property,
																											<property
																												key={<identifier name='setup' /> as estree.Identifier}
																												value={
																													<function-expression
																														params={[]}
																														body={
																															<block-statement
																																body={[
																																	<return-statement
																																		argument={
																																			<object-expression
																																				properties={[
																																					<property
																																						key={<identifier name='args' /> as estree.Identifier}
																																						value={<identifier name='args' /> as estree.Identifier}
																																						kind={'init' as const}
																																						shorthand
																																					/> as estree.Property,
																																				]}
																																			/> as estree.ObjectExpression
																																		}
																																	/> as estree.ReturnStatement,
																																]}
																															/> as estree.BlockStatement
																														}
																													/> as estree.FunctionExpression
																												}
																												method
																												kind={'init' as const}
																											/> as estree.Property,
																											<property
																												key={<identifier name='computed' /> as estree.Identifier}
																												value={
																													<object-expression
																														properties={[
																															<property
																																key={<identifier name='props' /> as estree.Identifier}
																																value={
																																	<function-expression
																																		params={[]}
																																		body={
																																			<block-statement
																																				body={[
																																					<return-statement
																																						argument={
																																							<object-expression
																																								properties={[
																																									<spread-element
																																										argument={
																																											<member-expression
																																												object={<this-expression /> as estree.ThisExpression}
																																												property={<identifier name='args' /> as estree.Identifier}
																																											/> as estree.MemberExpression
																																										}
																																									/> as estree.SpreadElement,
																																								]}
																																							/> as estree.ObjectExpression
																																						}
																																					/> as estree.ReturnStatement,
																																				]}
																																			/> as estree.BlockStatement
																																		}
																																	/> as estree.FunctionExpression
																																}
																																method
																																kind={'init' as const}
																															/> as estree.Property,
																														]}
																													/> as estree.ObjectExpression
																												}
																												kind={'init' as const}
																											/> as estree.Property,
																											<property
																												key={<identifier name='template' /> as estree.Identifier}
																												value={<literal value={`<${identifier.name} v-bind="props" />`} /> as estree.Literal}
																												kind={'init' as const}
																											/> as estree.Property,
																										]}
																									/> as estree.ObjectExpression
																								}
																							/> as estree.ReturnStatement,
																						]}
																					/> as estree.BlockStatement
																				}
																			/> as estree.FunctionExpression
																		}
																		method
																		kind={'init' as const}
																	/> as estree.Property,
																	<property
																		key={<identifier name='parameters' /> as estree.Identifier}
																		value={parameters}
																		kind={'init' as const}
																	/> as estree.Property,
																]}
															/> as estree.ObjectExpression
														}
														reference={<identifier name={`StoryObj<typeof ${identifier.name}>`} /> as estree.Identifier}
													/> as estree.Expression
												}
											/> as estree.VariableDeclarator,
										]}
									/> as estree.VariableDeclaration
								}
							/> as estree.ExportNamedDeclaration,
						]),
				<export-default-declaration
					declaration={(<identifier name='meta' />) as estree.Identifier}
				/> as estree.ExportDefaultDeclaration,
			]}
		/> as estree.Program;
	return format(
		'/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' +
			'/* eslint-disable import/no-default-export */\n' +
			'/* eslint-disable import/no-duplicates */\n' +
			'/* eslint-disable import/order */\n' +
			generate(program, { generator }) +
			(hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''),
		{
			parser: 'babel-ts',
			singleQuote: true,
			useTabs: true,
		}
	);
}

// glob('src/{components,pages,ui,widgets}/**/*.vue')
(async () => {
	const globs = await Promise.all([
		glob('src/components/global/Mk*.vue'),
		glob('src/components/global/RouterView.vue'),
		glob('src/components/Mk[A-E]*.vue'),
		glob('src/components/MkFlashPreview.vue'),
		glob('src/components/MkGalleryPostPreview.vue'),
		glob('src/components/MkSignupServerRules.vue'),
		glob('src/components/MkUserSetupDialog.vue'),
		glob('src/components/MkUserSetupDialog.*.vue'),
		glob('src/components/MkInstanceCardMini.vue'),
		glob('src/components/MkInviteCode.vue'),
		glob('src/pages/search.vue'),
		glob('src/pages/user/home.vue'),
	]);
	const components = globs.flat();
	await Promise.all(components.map(async (component) => {
		const stories = component.replace(/\.vue$/, '.stories.ts');
		await writeFile(stories, await toStories(component));
	}))
})();