<template>
	<view class="cashier-page">
		<view class="mine-entry" @tap="goMine">
			<text class="ri-user-3-line mine-entry-icon"></text>
			<text>我的</text>
		</view>

		<block v-if="!hasMerchantUrl">
			<view class="empty-page">
				<image class="empty-bg" :src="emptyStateBackground" mode="aspectFill" />
				<view class="empty-overlay"></view>
				<view class="empty-inner">
					<view class="empty-brand-block">
						<text class="empty-brand">{{ brandName }}</text>
						<text class="empty-subtitle">扫描商家收款码，快速完成付款</text>
					</view>
					<view class="empty-action">
						<button class="empty-scan-button" hover-class="empty-scan-button-hover" :loading="scanning" @tap="handleScan">
							<text class="ri-qr-scan-2-line empty-scan-button-icon"></text>
							<text>扫一扫商家收款码</text>
						</button>
						<view class="empty-mine-link" hover-class="empty-mine-link-hover" @tap="goMine">
							<text class="ri-user-3-line empty-mine-link-icon"></text>
							<text>个人中心</text>
						</view>
					</view>
				</view>
			</view>
		</block>

		<block v-else>
		<view class="pay-header" :style="{ backgroundImage: `url(${merchant.background})` }">
			<view class="pay-header-mask"></view>
			<view class="pay-header-scrim"></view>
			<view class="merchant-label">
				<view class="merchant-label-inner">
					<text class="muted">向</text>
					<text class="merchant-name" :style="{ fontSize: merchantNameFontSize }">{{ merchant.name }}</text>
					<text class="muted">付款</text>
				</view>
			</view>
		</view>

		<view class="pay-body">
			<view class="pay-main">
				<view class="amount-zone">
					<view class="amount-display">
						<text class="currency">¥</text>
						<text v-if="amount" class="amount-text">{{ amount }}</text>
						<view v-else class="cursor"></view>
					</view>
					<text v-if="!amount" class="amount-hint">请输入付款金额</text>
					<view v-if="amount && showBenefitTip" class="benefit-block">
						<view class="benefit-tip">
							{{ benefitTitle }}
							<text class="benefit-value">¥ {{ accumulationAmount }}</text>
						</view>
					</view>
				</view>

				<view class="extra-zone">
					<view class="remark-row">
						<input
							v-model="remark"
							class="remark-input"
							placeholder="付款备注（选填）"
							placeholder-class="remark-placeholder"
							:disabled="paying"
						/>
					</view>
					<!-- #ifdef MP-ALIPAY -->
					<button
						v-if="showAlipayPhoneAuthBtn"
						class="property-row property-row-btn"
						hover-class="property-row-hover"
						open-type="getAuthorize"
						scope="phoneNumber"
						:disabled="paying || bindingPhone"
						@tap="handleAlipayPhoneAuthTap"
						@getAuthorize="handleAlipayPhoneAuthorize"
						@error="handleAlipayPhoneAuthError"
					>
						<text class="property-name li-single-line property-name-action">
							{{ propertyRowText }}
						</text>
						<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
					</button>
					<view
						v-else
						class="property-row"
						hover-class="property-row-hover"
						@tap.stop="handlePropertyRow"
					>
						<text
							class="property-name li-single-line"
							:class="{
								'property-name-action': isPropertyActionText && !alipayPropertyUnbound,
								'property-name-tip': alipayPropertyUnbound
							}"
						>
							{{ propertyRowText }}
						</text>
						<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
					</view>
					<!-- #endif -->
					<!-- #ifndef MP-ALIPAY -->
					<view
						class="property-row"
						hover-class="property-row-hover"
						@tap.stop="handlePropertyRow"
					>
						<text
							class="property-name li-single-line"
							:class="{ 'property-name-action': isPropertyActionText }"
						>
							{{ propertyRowText }}
						</text>
						<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
					</view>
					<!-- #endif -->
				</view>
			</view>
		</view>

		<view v-if="propertySheetVisible" class="property-sheet-mask" @tap="closePropertySheet">
			<view class="property-sheet" @tap.stop>
				<view class="property-sheet-header">
					<text class="property-sheet-title">{{ propertySheetTitle }}</text>
					<text class="ri-close-line property-sheet-close" @tap="closePropertySheet"></text>
				</view>
				<scroll-view scroll-y class="property-sheet-list">
					<view
						v-for="item in propertyList"
						:key="String(item.merchant_id)"
						class="property-sheet-item"
						:class="{ active: isSameMerchantId(item.merchant_id, propertyMerchantId) }"
						@tap="handleSwitchProperty(item)"
					>
						<text class="property-sheet-item-name li-two-line">{{ item.merchant_name }}</text>
						<text
							v-if="isSameMerchantId(item.merchant_id, propertyMerchantId)"
							class="ri-check-line property-sheet-item-check"
						></text>
					</view>
				</scroll-view>
				<view v-if="!IS_ALIPAY" class="property-sheet-footer" @tap="goBindProperty">
					<text class="ri-add-line property-sheet-footer-icon"></text>
					<text>绑定新房源</text>
				</view>
			</view>
		</view>

		<view class="keyboard" :class="{ disabled: paying }">
			<view class="keypad">
				<view class="number-grid">
					<view v-for="key in numberKeys" :key="key" class="key" @tap="appendKey(key)">
						{{ key }}
					</view>
					<view class="key key-zero" @tap="appendKey('0')">0</view>
					<view class="key" @tap="appendKey('.')">.</view>
				</view>
				<view class="action-column">
					<view class="key delete-key" @tap="deleteKey">
						<text class="ri-delete-back-2-line"></text>
					</view>
					<view
						class="confirm-key"
						:class="{ disabled: !canPay, loading: paying }"
						@tap="handlePay"
					>
						<block v-if="paying">支付中</block>
						<block v-else-if="canPay">
							<text>确认支付</text>
							<text class="confirm-amount">¥{{ payableAmount }}</text>
						</block>
						<block v-else>确认支付</block>
					</view>
				</view>
			</view>
			<view class="safe-bottom"></view>
		</view>
		</block>
	</view>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
import type { MerchantListItem } from '@/api/cashier';
import { bindMobile, fetchOpenInfo, resolveLoginPlatform } from '@/api/auth';
import { postForm } from '@/utils/request';
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
import {
	getPropertyBindingCacheKey,
	getPropertyBindingCacheKeyByHash,
	getSupplierCacheKey,
	getSupplierCacheKeyByHash,
	clearPendingMerchantUrl,
	clearActiveMerchantContext,
	resolveMerchantUrlFromEntry,
	resolveMerchantUrlFromScanResult,
	syncActiveMerchantContext
} from '@/utils/merchant-context';
import { authStorage, encryptedStorage } from '@/utils/storage';
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay';

interface MerchantInfo {
	id: string;
	name: string;
	background: string;
	accumulationRate: number;
}

const defaultMerchant = (uni as any).$globalData?.defaultMerchant || DEFAULT_MERCHANT;
const PROPERTY_CACHE_TTL_SECONDS = 10 * 60;
const SUPPLIER_CACHE_TTL_SECONDS = 10 * 60;
const emptyStateBackground = '/static/cashier/bg-image.png';
const brandName = '卓享汇支付';
const IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';

const hasMerchantUrl = ref(false);
const merchant = ref<MerchantInfo>({ ...defaultMerchant });
const amount = ref('');
const remark = ref('');
const paying = ref(false);
const activeMerchantUrl = ref('');
const scoreName = ref('物业公积金');
const propertyName = ref('');
const propertyMerchantId = ref('');
const propertyList = ref<MerchantListItem[]>([]);
const propertySheetVisible = ref(false);
const switchingProperty = ref(false);
const bindingPhone = ref(false);
const alipayPropertyUnbound = ref(false);
const scanning = ref(false);
/** 已判定无效的进页码，避免 onShow 用同一 launch qrCode 反复弹窗 */
const rejectedMerchantUrl = ref('');
const numberKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];

const canPay = computed(() => Number(amount.value) > 0 && !paying.value);
const payableAmount = computed(() => Number(amount.value || 0).toFixed(2));
const accumulationAmount = computed(() => {
	const value = Number(amount.value || 0) * Number(merchant.value.accumulationRate || 0);
	return value.toFixed(2);
});
const showBenefitTip = computed(() => Number(merchant.value.accumulationRate) > 0);
const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公积金'}`);
/** 商户名越长字号越小，尽量单行展示「向 xxx 付款」 */
const merchantNameFontSize = computed(() => {
	const len = String(merchant.value.name || '').trim().length;
	if (len <= 6) return '34rpx';
	if (len <= 10) return '30rpx';
	if (len <= 14) return '26rpx';
	if (len <= 18) return '24rpx';
	return '22rpx';
});
const propertyRowText = computed(() => {
	if (propertyName.value) return propertyName.value;
	if (propertyList.value.length) return '选择物业';
	if (IS_ALIPAY) {
		if (alipayPropertyUnbound.value) {
			return `未绑定，微信搜「${PROPERTY_APP.WECHAT_NAME}」绑定房源`;
		}
		return '绑定手机号查看房源';
	}
	return '业主去绑定';
});
const isPropertyActionText = computed(() => !propertyName.value);
const showAlipayPhoneAuthBtn = computed(
	() => IS_ALIPAY && !propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value
);
const showPropertyArrow = computed(() => true);
const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业'));

interface PropertyBindingCache {
	merchant_id?: string;
	merchant_name?: string;
	score_name?: string;
	fee_rate?: string | number;
}

interface SupplierCache {
	supplier_id?: string | number;
	supplier_name?: string;
	logo?: string;
	fee_rate?: string | number;
}

const parseFeeRate = (feeRate: unknown) => {
	if (feeRate === undefined || feeRate === null || feeRate === '') return 0;
	const rate = Number(feeRate);
	if (!Number.isFinite(rate) || rate <= 0) return 0;
	return rate / 100;
};

const hasFeeRate = (feeRate: unknown) => feeRate !== undefined && feeRate !== null && feeRate !== '';

const resolveFeeRateFromPayload = (payload: Record<string, any> = {}) => {
	const supplierData = payload.supplier || {};
	return supplierData.fee_rate != null ? supplierData.fee_rate : payload.fee_rate;
};

const resolvePropertyFromPayload = (payload: Record<string, any> = {}) => {
	const merchantData = payload.merchant || payload;
	return {
		merchant_id: merchantData.merchant_id,
		merchant_name: merchantData.merchant_name,
		score_name: merchantData.config?.score_name
	};
};

const hasBindingPayload = (payload?: Record<string, any> | null) => {
	if (!payload || typeof payload !== 'object') return false;
	const merchantData = payload.merchant || payload;
	const merchantId = merchantData.merchant_id;
	const merchantName = merchantData.merchant_name;
	return merchantId !== undefined && merchantId !== null && merchantId !== '' && !!merchantName;
};

const buildBindingCache = (payload: Record<string, any> = {}): PropertyBindingCache => {
	const merchantData = payload.merchant || payload;
	return {
		merchant_id: merchantData.merchant_id,
		merchant_name: merchantData.merchant_name,
		score_name: merchantData.config?.score_name,
		fee_rate: resolveFeeRateFromPayload(payload)
	};
};

const clearPropertyBinding = () => {
	propertyName.value = '';
	propertyMerchantId.value = '';
};

const fetchMerchantList = async (merchantUrl: string) => {
	const response = await merchantListByUrl(merchantUrl);
	if (Number(response?.code) !== 200) {
		throw new Error(response?.msg || '获取物业列表失败');
	}
	return Array.isArray(response?.data) ? response.data : [];
};

const applyPropertyInfo = (property?: {
	merchant_id?: string | number;
	merchant_name?: string;
	score_name?: string;
}) => {
	if (!property) return;
	if (property.merchant_id !== undefined && property.merchant_id !== null && property.merchant_id !== '') {
		propertyMerchantId.value = String(property.merchant_id);
	}
	if (property.merchant_name) {
		propertyName.value = property.merchant_name;
	}
	if (property.score_name) {
		scoreName.value = property.score_name;
	}
};

const isSameMerchantId = (left: string | number, right: string | number) => {
	return String(left) === String(right);
};

const buildSupplierCache = (payload: Record<string, any> = {}, previous?: SupplierCache | null): SupplierCache => {
	const supplierData = payload.supplier || {};
	return {
		supplier_id:
			supplierData.supplier_id != null
				? supplierData.supplier_id
				: payload.supplier_id != null
					? payload.supplier_id
					: previous?.supplier_id,
		supplier_name:
			supplierData.supplier_name != null
				? supplierData.supplier_name
				: payload.supplier_name != null
					? payload.supplier_name
					: previous?.supplier_name,
		logo: supplierData.logo != null ? supplierData.logo : payload.logo != null ? payload.logo : previous?.logo,
		fee_rate: (() => {
			const rate = resolveFeeRateFromPayload(payload);
			return rate != null ? rate : previous?.fee_rate;
		})()
	};
};

const applyMerchantInfo = (data: any = {}) => {
	const name = data.name || data.merchant_name || data.title;
	const background = data.background || data.banner || data.cover || data.bg;
	merchant.value = {
		id: data.id || data.merchant_id || merchant.value.id,
		name: name || merchant.value.name,
		background: background || merchant.value.background,
		accumulationRate:
			data.accumulationRate !== undefined
				? Number(data.accumulationRate)
				: merchant.value.accumulationRate
	};
};

const applyBindingInfo = (binding?: PropertyBindingCache | null) => {
	if (!binding) return;
	if (hasFeeRate(binding.fee_rate)) {
		applyMerchantInfo({
			accumulationRate: parseFeeRate(binding.fee_rate)
		});
	}
	applyPropertyInfo(binding);
};

const applySupplierInfo = (supplier?: SupplierCache | null) => {
	if (!supplier) return;
	const updates: Record<string, unknown> = {
		name: supplier.supplier_name,
		background: supplier.logo
	};
	if (hasFeeRate(supplier.fee_rate)) {
		updates.accumulationRate = parseFeeRate(supplier.fee_rate);
	}
	applyMerchantInfo(updates);
};

const syncBindingCacheForUrl = (merchantUrl: string) => {
	const context = syncActiveMerchantContext(merchantUrl);
	// Strong consistency: when switching merchant QR URL, clear previous merchant cache.
	if (context.switched) {
		encryptedStorage.remove(getPropertyBindingCacheKeyByHash(context.previousHash));
		encryptedStorage.remove(getSupplierCacheKeyByHash(context.previousHash));
	}
	return getPropertyBindingCacheKey(merchantUrl);
};

const resetToEmptyMerchantState = () => {
	hasMerchantUrl.value = false;
	activeMerchantUrl.value = '';
	merchant.value = { ...defaultMerchant };
	amount.value = '';
	remark.value = '';
	propertyName.value = '';
	propertyMerchantId.value = '';
	propertyList.value = [];
	propertySheetVisible.value = false;
	alipayPropertyUnbound.value = false;
	scoreName.value = '物业公积金';
	clearPendingMerchantUrl();
	clearActiveMerchantContext();
};

const promptScan = (message?: string) => {
	uni.showModal({
		title: '提示',
		content: message || '未识别到商家码，请扫描商家收款码进入收银台',
		confirmText: '去扫码',
		showCancel: false,
		success: (res) => {
			if (res.confirm) {
				handleScan();
			}
		}
	});
};

const failInvalidMerchant = (merchantUrl: string, message?: string) => {
	rejectedMerchantUrl.value = merchantUrl;
	resetToEmptyMerchantState();
	promptScan(message);
};

const fetchSupplierByUrl = async (merchantUrl: string) => {
	const response = await supplierByUrl(merchantUrl);
	if (Number(response?.code) !== 200 || !response?.data) {
		return {
			ok: false as const,
			msg: response?.msg || '未识别到商家码，请扫描商家收款码进入收银台'
		};
	}

	const payload = response.data || {};
	const cacheKey = getSupplierCacheKey(merchantUrl);
	const cached = encryptedStorage.get<SupplierCache>(cacheKey);
	const latestSupplier = buildSupplierCache(payload, cached);
	if (!latestSupplier.supplier_name && !latestSupplier.supplier_id) {
		return {
			ok: false as const,
			msg: response?.msg || '未识别到商家码，请扫描商家收款码进入收银台'
		};
	}

	applyPropertyInfo(resolvePropertyFromPayload(payload));
	applySupplierInfo(latestSupplier);
	encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
	return { ok: true as const, supplier: latestSupplier };
};

const initWithMerchantUrl = async (
	merchantUrl: string,
	options: { force?: boolean } = {}
) => {
	if (!options.force && rejectedMerchantUrl.value === merchantUrl) {
		resetToEmptyMerchantState();
		return;
	}

	try {
		const [supplierResult, bindResponse] = await Promise.all([
			fetchSupplierByUrl(merchantUrl),
			bindMerchantByUrl(merchantUrl).catch(() => null)
		]);

		if (!supplierResult.ok) {
			failInvalidMerchant(merchantUrl, supplierResult.msg);
			return;
		}

		const bindCode = Number(bindResponse?.code);
		const bindMsg = String(bindResponse?.msg || '');
		if (bindResponse && bindCode !== 200 && /重新扫码/.test(bindMsg)) {
			failInvalidMerchant(merchantUrl, bindMsg);
			return;
		}

		rejectedMerchantUrl.value = '';
		hasMerchantUrl.value = true;
		activeMerchantUrl.value = merchantUrl;
		propertyName.value = '';
		propertyMerchantId.value = '';
		propertyList.value = [];
		propertySheetVisible.value = false;
		alipayPropertyUnbound.value = false;
		scoreName.value = '物业公积金';
		applySupplierInfo(supplierResult.supplier);
		await loadPropertyBinding(merchantUrl);
	} catch (error: any) {
		failInvalidMerchant(
			merchantUrl,
			error?.message || '未识别到商家码，请扫描商家收款码进入收银台'
		);
	}
};

const handleScan = () => {
	if (scanning.value) return;

	scanning.value = true;
	uni.scanCode({
		onlyFromCamera: false,
		scanType: ['qrCode'],
		success: async (res) => {
			const merchantUrl = resolveMerchantUrlFromScanResult(res.result || '');
			if (!merchantUrl) {
				uni.showToast({
					title: '未识别到商家码',
					icon: 'none'
				});
				promptScan();
				return;
			}
			await initWithMerchantUrl(merchantUrl, { force: true });
		},
		fail: (err: any) => {
			const errMsg = err?.errMsg || '';
			if (errMsg.includes('cancel')) return;
			uni.showToast({
				title: '扫码失败，请重试',
				icon: 'none'
			});
		},
		complete: () => {
			scanning.value = false;
		}
	});
};

const handleMissingMerchantUrl = () => {
	promptScan();
};

const resolvePropertyListFallback = async (merchantUrl: string) => {
	try {
		const list = await fetchMerchantList(merchantUrl);
		propertyList.value = list;

		if (!list.length) {
			clearPropertyBinding();
			encryptedStorage.remove(syncBindingCacheForUrl(merchantUrl));
		}
	} catch (error) {
		console.log('property list fallback', error);
		clearPropertyBinding();
		propertyList.value = [];
	}
};

/**
 * 支付宝：无物业名/列表时，用 open.info 的 user_id 判断是否已绑过手机号。
 * 有 user_id → 显示「未绑定，微信搜「乐享云…」绑定房源」；否则 → 「绑定手机号查看房源」。
 */
const resolveAlipayEmptyPropertyState = async (options: { assumeBoundIfEmpty?: boolean } = {}) => {
	if (!IS_ALIPAY) return;

	if (propertyName.value || propertyList.value.length) {
		alipayPropertyUnbound.value = false;
		return;
	}

	try {
		const response = await fetchOpenInfo();
		const userId = response?.data?.user?.user_id ?? response?.data?.open?.user_id;
		if (userId != null && String(userId).trim() !== '') {
			alipayPropertyUnbound.value = true;
			return;
		}
	} catch (error) {
		console.log('open.info empty property state', error);
	}

	alipayPropertyUnbound.value = !!options.assumeBoundIfEmpty;
};

const loadPropertyBinding = async (merchantUrl: string, options: { skipFallback?: boolean } = {}) => {
	const cacheKey = syncBindingCacheForUrl(merchantUrl);
	const cached = encryptedStorage.get<PropertyBindingCache>(cacheKey);
	if (cached?.merchant_name) {
		applyBindingInfo(cached);
	}

	try {
		const response = await bindMerchantByUrl(merchantUrl);
		if (Number(response?.code) !== 200 || !hasBindingPayload(response?.data)) {
			if (!options.skipFallback) {
				await resolvePropertyListFallback(merchantUrl);
			}
			await resolveAlipayEmptyPropertyState();
			return;
		}

		const latestBinding = buildBindingCache(response.data || {});
		applyBindingInfo(latestBinding);
		encryptedStorage.set(cacheKey, latestBinding, PROPERTY_CACHE_TTL_SECONDS);
		await resolveAlipayEmptyPropertyState();
	} catch (error) {
		console.log('bind merchant fallback cache', error);
		if (!options.skipFallback) {
			await resolvePropertyListFallback(merchantUrl);
		}
		await resolveAlipayEmptyPropertyState();
	}
};

const loadSupplier = async (merchantUrl: string) => {
	const cacheKey = getSupplierCacheKey(merchantUrl);
	const cached = encryptedStorage.get<SupplierCache>(cacheKey);
	if (cached) {
		applySupplierInfo(cached);
	}

	try {
		const result = await fetchSupplierByUrl(merchantUrl);
		if (!result.ok) return false;
		return true;
	} catch (error) {
		console.log('supplier fallback cache', error);
		return false;
	}
};

const appendKey = (key: string) => {
	if (paying.value) return;

	if (key === '.') {
		if (!amount.value) {
			amount.value = '0.';
			return;
		}
		if (amount.value.includes('.')) return;
		amount.value += '.';
		return;
	}

	const nextValue = amount.value === '0' ? key : `${amount.value}${key}`;
	const decimalPart = nextValue.split('.')[1];
	if (decimalPart && decimalPart.length > 2) return;
	if (Number(nextValue) > 999999.99) return;
	amount.value = nextValue;
};

const deleteKey = () => {
	if (paying.value) return;
	amount.value = amount.value.slice(0, -1);
};

const showAlipayUnboundTip = () => {
	uni.showModal({
		title: '暂未绑定房源',
		content: `请打开微信，搜索并进入「${PROPERTY_APP.WECHAT_NAME}」小程序，完成房源绑定后消费即可获得物业基金。`,
		showCancel: false,
		confirmText: '知道了'
	});
};

const refreshPropertyAfterPhoneBind = async () => {
	if (!activeMerchantUrl.value) return;

	alipayPropertyUnbound.value = false;
	clearPropertyBinding();
	propertyList.value = [];
	encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
	await loadPropertyBinding(activeMerchantUrl.value);
	// 刚绑完手机号仍无房源时，兜底按已绑定处理（open.info 偶发未带上 user_id）
	if (!propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value) {
		await resolveAlipayEmptyPropertyState({ assumeBoundIfEmpty: true });
	}

	if (propertyName.value || propertyList.value.length) {
		uni.showToast({
			title: '已匹配到物业信息',
			icon: 'none'
		});
	}
};

const tryParseJson = (value: any) => {
	if (typeof value !== 'string') return value;
	const text = value.trim();
	if (!text || (text[0] !== '{' && text[0] !== '[')) return value;
	try {
		return JSON.parse(text);
	} catch {
		return value;
	}
};

/** 调试日志用：把嵌套 JSON 字符串展开成可展开对象 */
const toLogObject = (value: any, depth = 0): any => {
	if (value == null || depth > 4) return value;
	const parsed = tryParseJson(value);
	if (Array.isArray(parsed)) {
		return parsed.map((item) => toLogObject(item, depth + 1));
	}
	if (parsed && typeof parsed === 'object') {
		const result: Record<string, any> = {};
		Object.keys(parsed).forEach((key) => {
			result[key] = toLogObject(parsed[key], depth + 1);
		});
		return result;
	}
	return parsed;
};

const logPhoneAuth = (label: string, payload?: any) => {
	console.log(`[收银台][手机号授权] ${label}`, payload === undefined ? undefined : toLogObject(payload));
};

/** 取 getPhoneNumber 密文：response.response */
const extractAlipayEncryptedData = (phoneRes: any) => {
	const parsed = tryParseJson(phoneRes?.response);
	if (parsed && typeof parsed === 'object') {
		const encrypted = parsed.response;
		return typeof encrypted === 'string' ? encrypted.trim() : '';
	}
	return typeof phoneRes?.response === 'string' ? phoneRes.response.trim() : '';
};

const requestAlipayPhoneNumber = () =>
	new Promise<any>((resolve, reject) => {
		// #ifdef MP-ALIPAY
		if (typeof my !== 'undefined' && typeof my.getPhoneNumber === 'function') {
			my.getPhoneNumber({
				success: resolve,
				fail: reject
			});
			return;
		}
		// #endif

		uni.getPhoneNumber({
			success: resolve,
			fail: reject
		});
	});

const handleAlipayPhoneAuthTap = () => {
	logPhoneAuth('按钮点击');
};

const handleAlipayPhoneAuthError = (event?: any) => {
	logPhoneAuth('error 回调', event);
};

const handleAlipayPhoneAuthorize = async (event?: any) => {
	logPhoneAuth('getAuthorize 回调', event);

	if (paying.value || bindingPhone.value) return;

	bindingPhone.value = true;
	try {
		const phoneRes = await requestAlipayPhoneNumber();
		logPhoneAuth('getPhoneNumber 成功', phoneRes);

		const encryptedData = extractAlipayEncryptedData(phoneRes);
		logPhoneAuth('解析 encryptedData', {
			encryptedData,
			response: tryParseJson(phoneRes?.response)
		});

		if (!encryptedData) {
			throw new Error('手机号授权失败');
		}

		const response = await bindMobile(encryptedData);
		logPhoneAuth('bind.mobile 响应', response);

		if (Number(response?.code) !== 200) {
			throw new Error(response?.msg || '手机号绑定失败');
		}

		const nextToken = String(response?.data?.token || '').trim();
		if (!nextToken) {
			throw new Error('绑定成功但未返回 token');
		}
		authStorage.setToken(nextToken, Number(response?.data?.expires_in || 0));
		logPhoneAuth('已替换登录 token', { token: nextToken });

		await refreshPropertyAfterPhoneBind();
	} catch (error: any) {
		logPhoneAuth('授权失败', error);
		const errMsg = error?.errMsg || error?.message || '';
		if (errMsg.includes('cancel') || errMsg.includes('拒绝')) return;
		uni.showToast({
			title: errMsg || '手机号授权失败',
			icon: 'none'
		});
	} finally {
		bindingPhone.value = false;
	}
};

const goBindProperty = () => {
	if (IS_ALIPAY) {
		showAlipayUnboundTip();
		return;
	}

	if (paying.value) return;
	propertySheetVisible.value = false;

	if (activeMerchantUrl.value) {
		encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
	}

	uni.navigateToMiniProgram({
		appId: PROPERTY_APP.APP_ID,
		path: PROPERTY_APP.BIND_HOUSE_PATH,
		extraData: { from: 'cashier' },
		envVersion: APP_CONFIG.API_BASE_URL.includes('dev.') ? 'trial' : 'release',
		fail: () => {
			uni.showToast({
				title: '跳转失败，请稍后重试',
				icon: 'none'
			});
		}
	});
};

const goMine = () => {
	uni.navigateTo({
		url: '/pages/mine/index'
	});
};

const closePropertySheet = () => {
	propertySheetVisible.value = false;
};

const openPropertySheet = async () => {
	if (!activeMerchantUrl.value) return;

	const list = await fetchMerchantList(activeMerchantUrl.value);
	if (!list.length) {
		if (IS_ALIPAY) {
			if (alipayPropertyUnbound.value) {
				showAlipayUnboundTip();
			}
			return;
		}
		goBindProperty();
		return;
	}

	propertyList.value = list;
	propertySheetVisible.value = true;
};

const handlePropertyRow = async () => {
	if (paying.value || switchingProperty.value || bindingPhone.value) return;
	if (!activeMerchantUrl.value) return;

	if (IS_ALIPAY && alipayPropertyUnbound.value) {
		showAlipayUnboundTip();
		return;
	}

	if (IS_ALIPAY && showAlipayPhoneAuthBtn.value) {
		return;
	}

	if (!propertyName.value) {
		try {
			if (propertyList.value.length) {
				propertySheetVisible.value = true;
				return;
			}
			await openPropertySheet();
		} catch (error: any) {
			uni.showToast({
				title: error?.message || '获取物业列表失败',
				icon: 'none'
			});
		}
		return;
	}

	try {
		await openPropertySheet();
	} catch (error: any) {
		uni.showToast({
			title: error?.message || '获取物业列表失败',
			icon: 'none'
		});
	}
};

const switchToProperty = async (
	item: MerchantListItem,
	merchantUrl: string,
	options: { closeSheet?: boolean; skipBindingFallback?: boolean } = {}
) => {
	if (paying.value || switchingProperty.value) return false;
	if (isSameMerchantId(item.merchant_id, propertyMerchantId.value)) {
		if (options.closeSheet) closePropertySheet();
		return true;
	}

	switchingProperty.value = true;
	try {
		const response = await changeMerchant({
			merchant_id: item.merchant_id,
			url: merchantUrl
		});
		if (Number(response?.code) !== 200) {
			throw new Error(response?.msg || '切换物业失败');
		}

		if (options.closeSheet) closePropertySheet();
		applyPropertyInfo(item);
		encryptedStorage.set(
			getPropertyBindingCacheKey(merchantUrl),
			{
				merchant_id: item.merchant_id,
				merchant_name: item.merchant_name
			},
			PROPERTY_CACHE_TTL_SECONDS
		);
		await loadPropertyBinding(merchantUrl, {
			skipFallback: options.skipBindingFallback != null ? options.skipBindingFallback : true
		});
		await loadSupplier(merchantUrl);
		return true;
	} catch (error: any) {
		uni.showToast({
			title: error?.message || '切换物业失败',
			icon: 'none'
		});
		return false;
	} finally {
		switchingProperty.value = false;
	}
};

const handleSwitchProperty = async (item: MerchantListItem) => {
	if (!activeMerchantUrl.value) return;
	await switchToProperty(item, activeMerchantUrl.value, { closeSheet: true, skipBindingFallback: true });
};

const buildResultUrl = (tradeNo: string) => {
	const query = [
		`amount=${payableAmount.value}`,
		`accumulation=${accumulationAmount.value}`,
		`merchant_name=${encodeURIComponent(merchant.value.name)}`,
		`merchant_url=${encodeURIComponent(activeMerchantUrl.value)}`,
		`trade_no=${encodeURIComponent(tradeNo)}`,
		`score_name=${encodeURIComponent(scoreName.value)}`
	].join('&');
	return `/pages/cashier/result?${query}`;
};

const handlePay = async () => {
	if (!canPay.value) return;

	paying.value = true;
	try {
		if (!activeMerchantUrl.value) {
			throw new Error('缺少支付参数：商户码 url');
		}

		// 扫商户码支付：调用后端 jspay，按当前端传入 platform
		const payload = {
			amount: payableAmount.value,
			platform: resolveLoginPlatform(),
			remark: remark.value.trim(),
			url: activeMerchantUrl.value
		};

		const payRes = await postForm<any>('user/v1/jspay', payload);
		if (Number(payRes?.code) !== 200) {
			throw new Error(payRes?.msg || '获取支付参数失败');
		}

		const payData = payRes?.data || {};
		const tradeNo = payData.trade_no || payData.tradeNo || '';
		if (!tradeNo) {
			throw new Error('缺少交易单号');
		}

		try {
			await requestPlatformPayment(payData);
		} catch (error) {
			if (isPaymentCancelled(error)) {
				await cancelTrade(tradeNo).catch(() => {});
				throw new Error('已取消支付');
			}
			throw error;
		}

		uni.redirectTo({
			url: buildResultUrl(tradeNo)
		});
	} catch (error: any) {
		uni.showToast({
			title: formatPaymentError(error),
			icon: 'none'
		});
	} finally {
		paying.value = false;
	}
};

onLoad((options: any) => {
	const merchantUrl = resolveMerchantUrlFromEntry(options || {});
	if (!merchantUrl) {
		handleMissingMerchantUrl();
		return;
	}
	clearPendingMerchantUrl();
	initWithMerchantUrl(merchantUrl);
});

onShow(() => {
	if (!hasMerchantUrl.value || !activeMerchantUrl.value) {
		const merchantUrl = resolveMerchantUrlFromEntry({});
		if (merchantUrl && merchantUrl !== rejectedMerchantUrl.value) {
			clearPendingMerchantUrl();
			initWithMerchantUrl(merchantUrl);
			return;
		}
	}

	if (!hasMerchantUrl.value || !activeMerchantUrl.value) return;
	loadPropertyBinding(activeMerchantUrl.value);
	loadSupplier(activeMerchantUrl.value);
});
</script>

<style lang="scss" scoped>
.cashier-page {
	display: flex;
	flex-direction: column;
	min-height: 100vh;
	background: #FFFFFF;
	color: #111827;
	position: relative;
}

.mine-entry {
	position: fixed;
	top: calc(env(safe-area-inset-top) + 16rpx);
	left: 24rpx;
	z-index: 20;
	display: flex;
	align-items: center;
	gap: 6rpx;
	padding: 12rpx 20rpx;
	border-radius: 999rpx;
	background: rgba(255, 255, 255, 0.92);
	color: #1f5bd8;
	font-size: 24rpx;
	font-weight: 700;
	box-shadow: 0 8rpx 20rpx rgba(17, 24, 39, 0.08);
}

.mine-entry-icon {
	font-size: 28rpx;
	line-height: 1;
}

.empty-page {
	position: relative;
	min-height: 100vh;
	overflow: hidden;
}

.empty-bg {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

.empty-overlay {
	position: absolute;
	inset: 0;
	background: linear-gradient(
		180deg,
		rgba(15, 35, 95, 0.18) 0%,
		rgba(15, 35, 95, 0.42) 42%,
		rgba(255, 255, 255, 0.92) 78%,
		#ffffff 100%
	);
}

.empty-inner {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	min-height: 100vh;
	padding: 160rpx 48rpx calc(72rpx + env(safe-area-inset-bottom));
	box-sizing: border-box;
}

.empty-brand-block {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
}

.empty-brand {
	font-family: SourceHanSansCN, -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
	color: #ffffff;
	font-size: 60rpx;
	font-weight: 600;
	line-height: 1.25;
	letter-spacing: 2rpx;
	text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
}

.empty-subtitle {
	margin-top: 20rpx;
	color: rgba(255, 255, 255, 0.92);
	font-size: 28rpx;
	line-height: 1.5;
	text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.18);
}

.empty-action {
	padding-top: 48rpx;
}

.empty-scan-button {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 104rpx;
	padding: 0;
	border-radius: 16rpx;
	background: #1f5bd8;
	color: #ffffff;
	font-size: 32rpx;
	font-weight: 800;
	line-height: 1;
	box-shadow: 0 12rpx 32rpx rgba(31, 91, 216, 0.28);
}

.empty-scan-button::after {
	border: none;
}

.empty-scan-button-icon {
	margin-right: 12rpx;
	font-size: 40rpx;
	line-height: 1;
}

.empty-scan-button-hover {
	background: #174dc1;
}

.empty-mine-link {
	display: flex;
	align-items: center;
	justify-content: center;
	margin-top: 24rpx;
	color: rgba(255, 255, 255, 0.96);
	font-size: 28rpx;
	font-weight: 600;
}

.empty-mine-link-hover {
	opacity: 0.86;
}

.empty-mine-link-icon {
	margin-right: 8rpx;
	font-size: 32rpx;
	line-height: 1;
}

.brand-only {
	margin: 0;
}

.pay-header {
	position: relative;
	flex-shrink: 0;
	height: 360rpx;
	background-size: cover;
	background-position: center center;
	overflow: hidden;
}

.pay-header-mask {
	position: absolute;
	inset: 0;
	z-index: 1;
	background: linear-gradient(180deg, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0) 42%);
}

.pay-header-scrim {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 1;
	height: 200rpx;
	background: linear-gradient(
		180deg,
		rgba(0, 0, 0, 0) 0%,
		rgba(0, 0, 0, 0.42) 48%,
		rgba(0, 0, 0, 0.62) 78%,
		rgba(255, 255, 255, 1) 100%
	);
}

.pay-body {
	flex: 1;
	margin-top: -24rpx;
	padding-bottom: 488rpx;
	background: #FFFFFF;
}

.pay-main {
	padding: 0 48rpx;
}

.merchant-label {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 52rpx;
	z-index: 2;
	display: flex;
	justify-content: center;
}

.merchant-label-inner {
	display: flex;
	align-items: baseline;
	flex-wrap: nowrap;
	max-width: 88%;
	padding: 14rpx 32rpx;
	border-radius: 999rpx;
	background: rgba(0, 0, 0, 0.42);
}

.muted {
	flex-shrink: 0;
	color: rgba(255, 255, 255, 0.9);
	font-size: 28rpx;
}

.merchant-name {
	min-width: 0;
	margin: 0 8rpx;
	color: #FFFFFF;
	font-size: 34rpx;
	font-weight: 800;
	line-height: 1.3;
	text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.35);
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}

.amount-zone {
	margin-top: 56rpx;
}

.amount-display {
	display: flex;
	align-items: center;
	justify-content: flex-start;
	min-height: 100rpx;
}

.currency {
	flex-shrink: 0;
	margin-right: 16rpx;
	color: #050505;
	font-size: 60rpx;
	font-weight: 700;
	line-height: 1;
}

.amount-text {
	color: #050505;
	font-size: 88rpx;
	font-weight: 800;
	line-height: 1;
	letter-spacing: -1rpx;
}

.cursor {
	width: 6rpx;
	height: 80rpx;
	border-radius: 8rpx;
	background: #1F5BD8;
	animation: blink 1.1s step-end infinite;
}

@keyframes blink {
	50% {
		opacity: 0;
	}
}

.amount-hint {
	display: block;
	margin-top: 16rpx;
	padding-left: 4rpx;
	color: #C2C7D0;
	font-size: 26rpx;
}

.benefit-block {
	margin-top: 20rpx;
	padding-left: 4rpx;
}

.benefit-tip {
	color: #8B929E;
	font-size: 26rpx;
	line-height: 1.5;
}

.benefit-value {
	margin-left: 8rpx;
	color: #4B6CB7;
	font-weight: 600;
}

.extra-zone {
	margin-top: 36rpx;
	padding-top: 28rpx;
	border-top: 1rpx solid #F0F2F5;
}

.remark-row {
	padding: 0 4rpx;
}

.remark-input {
	height: 44rpx;
	color: #1F2937;
	font-size: 28rpx;
	text-align: left;
}

.remark-placeholder {
	color: #C2C7D0;
}

.property-row {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: flex-start;
	margin-top: 28rpx;
	padding: 12rpx 4rpx;
}

.property-row--disabled {
	opacity: 0.92;
}

.property-row-btn {
	display: flex;
	align-items: center;
	justify-content: flex-start;
	width: 100%;
	margin: 0;
	padding: 12rpx 4rpx;
	border: none;
	background: transparent;
	line-height: 1;
	text-align: left;
}

.property-row-btn::after {
	border: none;
}

.property-row-hover {
	opacity: 0.88;
}

.property-name {
	flex: 0 1 auto;
	max-width: calc(100% - 36rpx);
	color: #374151;
	font-size: 28rpx;
	font-weight: 600;
}

.property-name-action {
	color: #1F5BD8;
}

.property-name-tip {
	color: #8B929E;
	font-weight: 500;
	text-decoration: underline;
	text-underline-offset: 4rpx;
}

.property-arrow {
	flex-shrink: 0;
	margin-left: 4rpx;
	color: #C2C7D0;
	font-size: 30rpx;
	line-height: 1;
}

.property-sheet-mask {
	position: fixed;
	inset: 0;
	z-index: 100;
	display: flex;
	align-items: flex-end;
	background: rgba(17, 24, 39, 0.42);
}

.property-sheet {
	width: 100%;
	max-height: 68vh;
	padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
	border-radius: 24rpx 24rpx 0 0;
	background: #ffffff;
}

.property-sheet-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 28rpx 32rpx 20rpx;
}

.property-sheet-title {
	color: #111827;
	font-size: 32rpx;
	font-weight: 800;
}

.property-sheet-close {
	color: #9CA3AF;
	font-size: 40rpx;
	line-height: 1;
}

.property-sheet-list {
	max-height: 48vh;
}

.property-sheet-item {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 20rpx;
	padding: 28rpx 32rpx;
	border-top: 1rpx solid #F3F4F6;
}

.property-sheet-item.active {
	background: #F8FAFF;
}

.property-sheet-item-name {
	flex: 1;
	min-width: 0;
	color: #1F2937;
	font-size: 28rpx;
	line-height: 1.5;
}

.property-sheet-item.active .property-sheet-item-name {
	color: #1F5BD8;
	font-weight: 700;
}

.property-sheet-item-check {
	flex-shrink: 0;
	color: #1F5BD8;
	font-size: 36rpx;
	line-height: 1;
}

.property-sheet-footer {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 8rpx;
	margin: 8rpx 32rpx 0;
	padding: 24rpx 0;
	border-top: 1rpx solid #F3F4F6;
	color: #1F5BD8;
	font-size: 28rpx;
	font-weight: 700;
}

.property-sheet-footer-icon {
	font-size: 32rpx;
	line-height: 1;
}

.keyboard {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 10;
	padding: 12rpx 12rpx 0;
	background: #F7F7F8;
	border-top: 1rpx solid #ECEEF2;
}

.keyboard.disabled {
	opacity: 0.72;
}

.keypad {
	display: flex;
	gap: 12rpx;
}

.number-grid {
	display: grid;
	flex: 1;
	grid-template-columns: repeat(3, 1fr);
	gap: 12rpx;
}

.action-column {
	display: grid;
	width: 168rpx;
	grid-template-rows: 96rpx 1fr;
	gap: 12rpx;
}

.key,
.confirm-key {
	display: flex;
	align-items: center;
	justify-content: center;
	height: 96rpx;
	border-radius: 8rpx;
	background: #FFFFFF;
	color: #050505;
	font-size: 44rpx;
	font-weight: 600;
	line-height: 1;
}

.key:active,
.confirm-key:active {
	opacity: 0.78;
}

.key-zero {
	grid-column: span 2;
}

.delete-key {
	color: #747B86;
	font-size: 40rpx;
}

.confirm-key {
	flex-direction: column;
	gap: 6rpx;
	height: auto;
	min-height: 204rpx;
	padding: 0 12rpx;
	background: #1F5BD8;
	color: #FFFFFF;
	font-size: 26rpx;
	font-weight: 700;
	text-align: center;
}

.confirm-amount {
	font-size: 22rpx;
	font-weight: 600;
	opacity: 0.92;
}

.confirm-key.disabled,
.confirm-key.loading {
	background: #C8D3EE;
}

.safe-bottom {
	height: calc(16rpx + env(safe-area-inset-bottom));
}
</style>
