<template>
	<Header class="sticky top-0 z-10 bg-white">
		<div
			class="flex w-full flex-col gap-2 md:flex-row md:items-center md:justify-between"
		>
			<div class="flex flex-row items-center gap-2">
				<!-- 标题 -->
				<Breadcrumbs
					:items="[
						{ label: '开发工具', route: '/database-analyzer' },
						{ label: '数据库分析器', route: '/database-analyzer' },
					]"
				/>
			</div>
			<div class="flex flex-row gap-2">
				<LinkControl
					class="cursor-pointer"
					:options="{ pagetype: 'Site', filters: { status: 'Active' } }"
					placeholder="选择一个站点"
					v-model="site"
				/>
				<Button
					iconLeft="refresh-ccw"
					variant="subtle"
					:loading="site && !isRequiredInformationReceived"
					:disabled="!site"
					@click="
						() =>
							fetchTableSchemas({
								reload: true,
							})
					"
				>
					<span class="md:hidden">架构</span>
					<span class="hidden md:inline">刷新架构</span>
				</Button>
			</div>
		</div>
	</Header>
	<div class="m-5">
		<!-- 主体 -->
		<div class="mt-2 flex flex-col" v-if="isRequiredInformationReceived">
			<!-- 数据库大小分析器 -->
			<div>
				<div class="flex flex-row items-center justify-between">
					<p class="text-base font-medium text-gray-800">
						数据库大小分解
					</p>
					<div class="flex flex-row gap-2">
						<Button @click="this.showTableSchemaSizeDetailsDialog = true">
							查看详情
						</Button>
						<Button
							@click="optimizeTable"
							:loading="this.$resources.optimizeTable.loading"
						>
							优化表
						</Button>
					</div>
				</div>

				<!-- 滑块 -->
				<div
					class="mb-4 mt-4 flex h-7 w-full cursor-pointer items-start justify-start overflow-clip rounded border bg-gray-50 pl-0"
					@click="showTableSchemaSizeDetailsDialog = true"
				>
					<div
						class="h-7"
						:style="{
							backgroundColor: '#E86C13',
							width: `${databaseSizeBreakup.data_size_percentage}%`,
						}"
					></div>
					<div
						class="h-7"
						:style="{
							backgroundColor: '#34BAE3',
							width: `${databaseSizeBreakup.index_size_percentage}%`,
						}"
					></div>
				</div>
				<!-- 表格 -->
				<div
					class="full flex w-full flex-col items-start justify-start overflow-y-auto rounded px-1.5"
				>
					<div class="flex w-full items-center justify-start gap-x-2 py-3">
						<div
							class="h-2 w-2 rounded-full"
							style="background-color: #e86c13"
						></div>
						<span class="text-sm text-gray-800">数据大小</span
						><span class="ml-auto text-sm text-gray-800">{{
							formatSizeInMB(this.databaseSizeBreakup.data_size)
						}}</span>
					</div>
					<div
						class="flex w-full items-center justify-start gap-x-2 border-t py-3"
					>
						<div
							class="h-2 w-2 rounded-full"
							style="background-color: #34bae3"
						></div>
						<span class="text-sm text-gray-800">索引大小</span
						><span class="ml-auto text-sm text-gray-800"
							>{{ formatSizeInMB(this.databaseSizeBreakup.index_size) }}
						</span>
					</div>
					<div
						class="flex w-full items-center justify-start gap-x-2 border-t py-3"
					>
						<div
							class="h-2 w-2 rounded-full"
							style="background-color: #e2e2e2"
						></div>
						<span class="text-sm text-gray-800">空闲空间</span
						><span class="ml-auto text-sm text-gray-800"
							>{{ formatSizeInMB(this.databaseSizeBreakup.free_size) }}
						</span>
					</div>
				</div>
			</div>

			<!-- 数据库进程 -->
			<ToggleContent
				class="mt-3"
				label="数据库进程"
subLabel="分析数据库的进程"
			>
				<template #actions>
					<div>
						<Button
							:loading="this.$resources.databaseProcesses.loading"
							loading-text="刷新中"
							icon-left="rotate-ccw"
							@click.stop="this.$resources.databaseProcesses.submit()"
							>刷新</Button
						>
					</div>
				</template>
				<template #default>
					<div
						v-if="this.$resources.databaseProcesses.loading"
						class="flex h-60 w-full items-center justify-center gap-2 text-base text-gray-700"
					>
						<Spinner class="w-4" /> 正在加载数据库进程
					</div>
					<ResultTable
						v-else
						class="mt-2"
						:columns="databaseProcesses.columns"
						:data="databaseProcesses.data"
						:alignColumns="alignColumns"
						:cellFormatters="cellFormatters"
						:fullViewFormatters="fullViewFormatters"
						actionHeaderLabel="终止"
						:actionComponent="DatabaseProcessKillButton"
						:actionComponentProps="{
							site: this.site,
						}"
						:enableCSVExport="false"
						:borderLess="true"
					/>
				</template>
			</ToggleContent>

			<!-- 查询信息 -->
			<ToggleContent
				class="mt-3"
				label="SQL 查询分析"
				subLabel="检查可能影响数据库性能的相关查询"
			>
				<div class="mt-1">
					<FTabs
						:tabs="queryTabs"
						v-model="queryTabIndex"
						v-if="queryTabs.length"
					>
						<template #tab-panel="{ tab }">
							<DatabasePerformanceSchemaDisabledNotice
								v-if="
									(tab.label === '耗时查询' ||
										tab.label === '全表扫描查询') &&
									!isPerformanceSchemaEnabled
								"
							/>
							<ResultTable
								v-else
								:columns="tab.columns"
								:data="tab.data"
								:alignColumns="alignColumns"
								:cellFormatters="cellFormatters"
								:fullViewFormatters="fullViewFormatters"
								:enableCSVExport="false"
								:borderLess="true"
								:isTruncateText="true"
							/>
						</template>
					</FTabs>
				</div>
			</ToggleContent>

			<!-- 索引信息 -->
			<ToggleContent
				class="mt-3"
				label="数据库索引分析"
				subLabel="分析数据库的索引"
			>
				<div class="mt-1">
					<FTabs
						:tabs="databaseIndexesTab"
						v-model="dbIndexTabIndex"
						v-if="databaseIndexesTab.length"
					>
						<template #tab-panel="{ tab }">
							<DatabasePerformanceSchemaDisabledNotice
								v-if="
									(tab.label === '未使用索引' ||
										tab.label === '建议索引') &&
									!isPerformanceSchemaEnabled
								"
							/>
							<div v-else-if="tab.label === '建议索引'">
								<div
									v-if="
										!isIndexSuggestionTriggered ||
										this.$resources.suggestDatabaseIndexes.loading ||
										this.fetchingDatabaseIndex
									"
									class="flex h-60 flex-col items-center justify-center gap-4"
								>
									<Button
										variant="outline"
										@click="
											() => {
												this.isIndexSuggestionTriggered = true;
												this.$resources.suggestDatabaseIndexes.submit();
											}
										"
										:loading="
											this.$resources.suggestDatabaseIndexes.loading ||
											this.fetchingDatabaseIndex
										"
										>建议索引</Button
									>
									<p class="text-base text-gray-700">
										分析可能需要一些时间
									</p>
								</div>
								<ResultTable
									v-else
									:columns="suggestedDatabaseIndexes.columns"
									:data="suggestedDatabaseIndexes.data"
									:alignColumns="alignColumns"
									:cellFormatters="cellFormatters"
									:fullViewFormatters="fullViewFormatters"
									:enableCSVExport="false"
									:borderLess="true"
									actionHeaderLabel="添加索引"
									:actionComponent="DatabaseAddIndexButton"
									:actionComponentProps="{
										site: this.site,
									}"
									:isTruncateText="true"
								/>
							</div>
							<ResultTable
								v-else
								:columns="tab.columns"
								:data="tab.data"
								:alignColumns="alignColumns"
:cellFormatters="cellFormatters"
								:fullViewFormatters="fullViewFormatters"
								:isTruncateText="true"
								:enableCSVExport="false"
								:borderLess="true"
							/>
						</template>
					</FTabs>
				</div>
			</ToggleContent>

			<DatabaseTableSchemaSizeDetailsDialog
				v-if="this.site"
				:site="this.site"
				:tableSchemas="tableSchemas"
				v-model="showTableSchemaSizeDetailsDialog"
				:viewSchemaDetails="viewTableSchemaDetails"
			/>

			<DatabaseTableSchemaDialog
				v-if="this.site"
				:site="this.site"
				:tableSchemas="tableSchemas"
				:pre-selected-schema="preSelectedSchemaForSchemaDialog"
				v-model="showTableSchemasDialog"
			/>
		</div>
		<div
			v-else-if="!site"
			class="flex h-full min-h-[80vh] w-full items-center justify-center gap-2 text-gray-700"
		>
			选择一个站点以开始
		</div>
		<div
			class="flex h-full min-h-[80vh] w-full items-center justify-center gap-2 text-gray-700"
			v-else
		>
			<Spinner class="w-4" /> 正在加载表结构
		</div>
	</div>
</template>
<script>
import Header from '../../../components/Header.vue';
import { Tabs, Breadcrumbs } from 'jingrow-ui';
import LinkControl from '../../../components/LinkControl.vue';
import ObjectList from '../../../components/ObjectList.vue';
import { h, markRaw } from 'vue';
import { toast } from 'vue-sonner';
import { formatValue } from '../../../utils/format';
import ToggleContent from '../../../components/ToggleContent.vue';
import ResultTable from '../../../components/devtools/database/ResultTable.vue';
import DatabaseProcessKillButton from '../../../components/devtools/database/DatabaseProcessKillButton.vue';
import DatabaseTableSchemaDialog from '../../../components/devtools/database/DatabaseTableSchemaDialog.vue';
import DatabaseTableSchemaSizeDetailsDialog from '../../../components/devtools/database/DatabaseTableSchemaSizeDetailsDialog.vue';
import DatabaseAddIndexButton from '../../../components/devtools/database/DatabaseAddIndexButton.vue';
import DatabasePerformanceSchemaDisabledNotice from '../../../components/devtools/database/DatabasePerformanceSchemaDisabledNotice.vue';

export default {
	name: 'DatabaseAnalyzer',
	components: {
		Header,
		Breadcrumbs,
		FTabs: Tabs,
		LinkControl,
		ObjectList,
		ToggleContent,
		ResultTable,
		DatabaseTableSchemaDialog,
		DatabaseTableSchemaSizeDetailsDialog,
		DatabaseProcessKillButton,
		DatabasePerformanceSchemaDisabledNotice,
	},
	data() {
		return {
			site: null,
			errorMessage: null,
			isIndexSuggestionTriggered: false,
			queryTabIndex: 0,
			dbIndexTabIndex: 0,
			showTableSchemaSizeDetailsDialog: false,
			preSelectedSchemaForSchemaDialog: null,
			showTableSchemasDialog: false,
			fetchingDatabaseIndex: false,
			DatabaseProcessKillButton: markRaw(DatabaseProcessKillButton),
			DatabaseAddIndexButton: markRaw(DatabaseAddIndexButton),
		};
	},
	mounted() {
		const url = new URL(window.location.href);
		const site_name = url.searchParams.get('site');
		if (site_name) {
			this.site = site_name;
		}
	},
	watch: {
		site(site_name) {
			if (!site_name) return;
			// set site to query param ?site=site_name
			const url = new URL(window.location.href);
			url.searchParams.set('site', site_name);
			window.history.pushState({}, '', url);

			// reset state
			this.data = null;
			this.errorMessage = null;
			this.fetchTableSchemas({
				site_name: site_name,
			});
			this.$resources.site.submit();
			this.$resources.databasePerformanceReport.submit({
				dt: 'Site',
				dn: site_name,
				method: 'get_database_performance_report',
			});
			this.$resources.databaseProcesses.submit({
				dt: 'Site',
				dn: site_name,
				method: 'fetch_database_processes',
			});
		},
	},
	resources: {
		site() {
			return {
				url: 'jcloud.api.client.get',
				initialData: {},
				makeParams: () => {
					return { pagetype: 'Site', name: this.site };
				},
				auto: false,
			};
		},
		tableSchemas() {
			return {
				url: 'jcloud.api.client.run_pg_method',
				initialData: {},
				auto: false,
makeParams: () => {
					return {
						dt: 'Site',
						dn: this.site,
						method: 'fetch_database_table_schema',
					};
				},
				onSuccess: (data) => {
					if (data?.message?.loading) {
						setTimeout(this.fetchTableSchemas, 5000);
					}
				},
			};
		},
		optimizeTable() {
			return {
				url: 'jcloud.api.client.run_pg_method',
				initialData: {},
				auto: false,
				onSuccess: (data) => {
					if (data?.message) {
						if (data?.message?.success) {
							toast.success(data?.message?.message);
							this.$router.push(
								`/sites/${this.site}/insights/jobs/${data?.message?.job_name}`,
							);
						} else {
							toast.error(data?.message?.message);
						}
					}
				},
			};
		},
		databasePerformanceReport() {
			return {
				url: 'jcloud.api.client.run_pg_method',
				initialData: {},
				makeParams: () => {
					return {
						dt: 'Site',
						dn: this.site,
						method: 'get_database_performance_report',
					};
				},
				auto: false,
			};
		},
		suggestDatabaseIndexes() {
			return {
				url: 'jcloud.api.client.run_pg_method',
				initialData: {},
				makeParams: () => {
					return {
						dt: 'Site',
						dn: this.site,
						method: 'suggest_database_indexes',
					};
				},
				onSuccess: (data) => {
					if (data?.message) {
						this.fetchingDatabaseIndex =
							this.$resources.suggestDatabaseIndexes?.data?.message?.loading ??
							false;
						if (this.fetchingDatabaseIndex) {
							setTimeout(() => {
								this.$resources.suggestDatabaseIndexes.submit();
							}, 5000);
						}
					}
				},
				auto: false,
			};
		},
		databaseProcesses() {
			return {
				url: 'jcloud.api.client.run_pg_method',
				initialData: {},
				makeParams: () => {
					return {
						dt: 'Site',
						dn: this.site,
						method: 'fetch_database_processes',
					};
				},
				auto: false,
			};
		},
	},
	computed: {
		site_info() {
			return this.$resources.site.data;
		},
		isRequiredInformationReceived() {
			if (this.$resources.site?.loading ?? true) return false;
			if (this.$resources.tableSchemas.loading) return false;
			if (this.$resources.tableSchemas?.data?.message?.loading) return false;
			if (!this.$resources.tableSchemas?.data?.message?.data) return false;
			if (this.$resources.tableSchemas?.data?.message?.data == {}) return false;
			if (!this.$resources.databasePerformanceReport?.data?.message)
				return false;
			return true;
		},
		tableSchemas() {
			if (!this.isRequiredInformationReceived) return [];
			let result = this.$resources.tableSchemas?.data?.message?.data ?? [];
			return result;
		},
		tableSizeInfo() {
			if (!this.isRequiredInformationReceived) return [];
			let data = [];
			for (const tableName in this.tableSchemas) {
				const table = this.tableSchemas[tableName];
				data.push({
					table_name: tableName,
					data_size_mb: (table.size.data_length / (1024 * 1024)).toFixed(3),
					index_size_mb: (table.size.index_length / (1024 * 1024)).toFixed(3),
					total_size_mb: (table.size.total_size / (1024 * 1024)).toFixed(3),
					no_of_columns: table.columns.length,
				});
			}
			return data;
		},
		tableAnalysisTableOptions() {
			if (!this.isRequiredInformationReceived) return [];
			return {
				data: () => this.tableSizeInfo,
				hideControls: true,
				columns: [
					{
						label: '表名',
						fieldname: 'table_name',
						width: 0.5,
						type: 'Component',
						component({ row }) {
							return h(
								'div',
								{
									class: 'truncate text-base cursor-copy',
									onClick() {
										if ('clipboard' in navigator) {
											navigator.clipboard.writeText(row.table_name);
											toast.success('已复制到剪贴板');
										}
									},
								},
								[row.table_name],
							);
						},
					},
					{
						label: '大小 (MB)',
						fieldname: 'total_size_mb',
width: 0.5,
					},
					{
						label: '列数',
						fieldname: 'no_of_columns',
						width: 0.5,
					},
				],
			};
		},
		databaseSizeBreakup() {
			if (!this.isRequiredInformationReceived) return null;
			let data_size = this.tableSizeInfo.reduce(
				(a, b) => a + parseFloat(b.data_size_mb),
				0,
			);
			data_size = data_size.toFixed(2);

			let index_size = this.tableSizeInfo.reduce(
				(a, b) => a + parseFloat(b.index_size_mb),
				0,
			);
			index_size = index_size.toFixed(2);

			let database_size_limit =
				this.site_info.current_plan.max_database_usage.toFixed(2);

			return {
				data_size,
				index_size,
				database_size_limit,
				free_size: (database_size_limit - data_size - index_size).toFixed(2),
				data_size_percentage: parseInt((data_size / database_size_limit) * 100),
				index_size_percentage: parseInt(
					(index_size / database_size_limit) * 100,
				),
			};
		},
		isPerformanceSchemaEnabled() {
			const result = this.$resources.databasePerformanceReport?.data?.message;
			if (!result) return false;
			return result['is_performance_schema_enabled'];
		},
		queryTabs() {
			if (!this.isRequiredInformationReceived) return [];
			const result = this.$resources.databasePerformanceReport?.data?.message;
			if (!result) return [];
			let prepared_result = [
				{
					label: '慢查询',
					columns: ['检查行数', '发送行数', '调用次数', '持续时间', '查询'],
					data: result['slow_queries'].map((e) => {
						return [e.rows_examined, e.rows_sent, e.count, e.duration, e.query];
					}),
				},
				{
					label: '耗时查询',
					columns: ['百分比', '调用次数', '平均时间', '查询'],
					data: result['top_10_time_consuming_queries'].map((e) => {
						return [
							Math.round(e['percent'], 1),
							e['calls'],
							e['avg_time_ms'],
							e['query'],
						];
					}),
				},
				{
					label: '全表扫描查询',
					columns: ['检查行数', '发送行数', '调用次数', '查询'],
					data: result['top_10_queries_with_full_table_scan'].map((e) => {
						return [e['rows_examined'], e['rows_sent'], e['calls'], e['query']];
					}),
				},
			];
			return prepared_result;
		},
		databaseIndexesTab() {
			if (!this.isRequiredInformationReceived) return [];
			const result = this.$resources.databasePerformanceReport?.data?.message;
			if (!result) return [];
			let prepared_result = [
				{
					label: '建议索引',
					columns: ['表', '列', '索引名称', '示例查询'],
					data: [],
				},
				{
					label: '冗余索引',
					columns: [
						'表名',
						'主导索引',
						'主导索引列',
						'冗余索引',
						'冗余索引列',
					],
					data: result['redundant_indexes'].map((e) => {
						return [
							e['table_name'],
							e['dominant_index_name'],
							e['dominant_index_columns'],
							e['redundant_index_name'],
							e['redundant_index_columns'],
						];
					}),
				},
				{
					label: '未使用索引',
					columns: ['表名', '索引名称'],
					data: result['unused_indexes'].map((e) => {
						return [e['table_name'], e['index_name']];
					}),
				},
			];
			return prepared_result;
		},
		suggestedDatabaseIndexes() {
			if (!this.isRequiredInformationReceived) return [];
			const result =
				this.$resources.suggestDatabaseIndexes?.data?.message?.data ?? [];
			let data = [];
			for (const record of result) {
				for (const index of record.suggested_indexes) {
					data.push([index.table, index.column, record.normalized]);
				}
			}
			return {
				columns: ['表', '列', '慢查询'],
				data: data,
			};
		},
		databaseProcesses() {
			if (!this.isRequiredInformationReceived) return null;
			const result = this.$resources.databaseProcesses.data?.message ?? [];
			return {
columns: ['ID', '状态', '时间', '用户', '主机', '命令', '查询'],
				data: result.map((e) => {
					return [
						e['id'],
						e['state'],
						e['time'],
						e['db_user'],
						e['db_user_host'],
						e['command'],
						e['query'],
					];
				}),
			};
		},
		cellFormatters() {
			return {
				'检查行数': (v) => formatValue(v, 'commaSeperatedNumber'),
				'发送行数': (v) => formatValue(v, 'commaSeperatedNumber'),
				调用次数: (v) => formatValue(v, 'commaSeperatedNumber'),
				'平均时间': (v) => formatValue(v, 'durationMilliseconds'),
				持续时间: (v) => formatValue(v, 'durationSeconds'),
				时间: (v) => formatValue(v, 'durationSeconds'),
			};
		},
		fullViewFormatters() {
			return {
				查询: (v) => formatValue(v, 'sql'),
			};
		},
		alignColumns() {
			return {
				百分比: 'right',
				'检查行数': 'right',
				'发送行数': 'right',
				调用次数: 'right',
				'平均时间': 'right',
				持续时间: 'right',
				时间: 'right',
			};
		},
	},
	methods: {
		fetchTableSchemas({ site_name = null, reload = false } = {}) {
			if (!site_name) site_name = this.site;
			if (!site_name) return;
			this.$resources.tableSchemas.submit({
				dt: 'Site',
				dn: site_name,
				method: 'fetch_database_table_schema',
				args: {
					reload,
				},
			});
		},
		optimizeTable() {
			this.$resources.optimizeTable.submit({
				dt: 'Site',
				dn: this.site,
				method: 'optimize_tables',
			});
		},
		viewTableSchemaDetails(tableName) {
			this.showTableSchemaSizeDetailsDialog = false;
			this.preSelectedSchemaForSchemaDialog = tableName;
			this.showTableSchemasDialog = true;
		},
		formatSizeInMB(mb) {
			try {
				let floatMB = parseFloat(mb);
				if (floatMB < 1) {
					let kb = floatMB * 1024; // Convert MB to KB
					return `${Math.round(kb)} KB`; // Return KB without decimal
				} else if (floatMB < 1024) {
					return `${Math.round(floatMB)} MB`; // Return MB without decimal
				} else {
					let gb = floatMB / 1024; // Convert MB to GB
					return `${gb.toFixed(1)} GB`; // Return GB with 1 decimal
				}
			} catch (error) {
				return `${mb} MB`; // Return MB without decimal
			}
		},
	},
};
</script>