import { BaseProvider } from '#templates/base/provider';
import { Resident } from '#models/Resident';
import { ResidentDocumentProvider } from '#providers/ResidentDocumentProvider';
import { ResidentStayProvider } from '#providers/ResidentStayProvider';
import { ResidentStay } from '#models/ResidentStay';
import { UserProvider } from '#providers/UserProvider';
import { User } from '#models/User';
import { UserAuth } from '#models/UserAuth';
import { Role } from '#models/Role';
import { UserRole as UserRoleModel } from '#models/UserRole';
import { PasswordService } from '#services/auth/passwordService';
import { UserRoleEnum } from '#constants/index';
import { GenericError } from '#interfaces/error/generic';
import { normalizePhone } from '#utils/stringUtils';
import { NestedFilterUtils } from '#utils/nestedFilterUtils';
import { Op } from 'sequelize';
import sequelize from '#services/database/sequelize/sequelizeService';
import { QueryTypes } from 'sequelize';
import { randomUUID } from 'crypto';
import { AuditLogService } from '#services/audit/auditLogService';

export class ResidentProvider extends BaseProvider<Resident> {
	public static instance: ResidentProvider;

	public static getInstance(): ResidentProvider {
		ResidentProvider.instance ??= new ResidentProvider();
		return ResidentProvider.instance;
	}

	public static get model() {
		return Resident;
	}

	constructor() {
		super('Resident');
	}

	override async delete(
		id: string,
		options?: {
			transaction?: any;
			force?: boolean;
			deleted_by?: string;
		},
	): Promise<string> {
		return this.executeInTransaction(async (tx) => {
			try {
				const toDelete = await Resident.findByPk(id, { transaction: tx });
				if (!toDelete) {
					throw new GenericError('NOT_FOUND', { resource: 'Resident', id });
				}

				// CRITICAL: Check for active resident stays only for hard delete
				if (options?.force) {
					const activeStayCount = await ResidentStay.count({
						where: {
							resident_id: id,
							move_out_at: null,
						},
						transaction: tx,
					});

					if (activeStayCount > 0) {
						throw new GenericError('VALIDATION_ERROR', {
							message:
								`Cannot hard delete resident with ${activeStayCount} active stay(s). ` +
								'Please check out resident first or use soft delete instead.',
							id: toDelete.getDataValue('id'),
							activeStayCount,
						});
					}
				}

				if (options?.force) {
					// Hard delete
					await toDelete.destroy({ transaction: tx, force: true });
				} else {
					// Soft delete - use UPDATE instead of DELETE
					const updateData: any = {
						deleted_at: new Date(),
					};
					if (options?.deleted_by) {
						updateData.deleted_by = options.deleted_by;
					}
					await toDelete.update(updateData, { transaction: tx });
				}

				// Log resident deletion
				await AuditLogService.enqueueSystemAudit({
					requestId: randomUUID(),
					traceId: randomUUID(),
					actorId: options?.deleted_by || null,
					actorName: options?.deleted_by ? undefined : 'System',
					actorEmail: options?.deleted_by ? undefined : 'system@bekind.vn',
					actorRole: options?.deleted_by ? undefined : 'SYSTEM',
					action: 'RESIDENT_DELETED',
					module: 'RESIDENT',
					entityId: id,
					entityType: 'Resident',
					description: `Resident ${toDelete.get('full_name')} deleted (force: ${options?.force || false})`,
					severity: 'HIGH',
					payload: {
						residentId: id,
						residentName: toDelete.get('full_name'),
						force: options?.force || false,
						deletedBy: options?.deleted_by,
					},
				} as any);

				await this.invalidateCache();
				return 'Successfully deleted item';
			} catch (error) {
				await this.logError(error as Error);
				throw error;
			}
		}, options?.transaction);
	}

	async createWithResidence(
		body: any,
		options?: {
			transaction?: any;
			skipUniqueCheck?: boolean;
			uniqueFields?: string[];
		},
	): Promise<Resident> {
		// Extract residence info (bed_ids, move_in_at) - keep user_id in residentData
		const { bed_ids, move_in_at, ...residentData } = body;
		const requestId = randomUUID();
		const traceId = randomUUID();

		// Wrap both operations in a single transaction
		return this.executeInTransaction(async (tx) => {
			// Create resident first (without user_id)
			const resident = await this.create(residentData, { ...options, transaction: tx });

			// Log resident creation
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: residentData.created_by || null,
				actorName: residentData.created_by ? undefined : 'System',
				actorEmail: residentData.created_by ? undefined : 'system@bekind.vn',
				actorRole: residentData.created_by ? undefined : 'SYSTEM',
				action: 'RESIDENT_CREATED',
				module: 'RESIDENT',
				entityId: resident.get('id'),
				entityType: 'Resident',
				description: `Resident ${resident.get('full_name')} created`,
				severity: 'MEDIUM',
				payload: {
					residentId: resident.get('id'),
					residentName: resident.get('full_name'),
					phone: resident.get('phone'),
					createdBy: residentData.created_by,
				},
			} as any);

			// If bed_ids array provided, create resident_stays for each bed_id
			if (bed_ids && Array.isArray(bed_ids) && bed_ids.length > 0) {
				const stayProvider = ResidentStayProvider.getInstance();
				const createdBy = residentData.created_by || residentData.updated_by;
				const updatedBy = residentData.updated_by || residentData.created_by;

				// Create a stay record for each bed_id
				for (const bed_id of bed_ids) {
					await stayProvider.create(
						{
							resident_id: resident.get('id'),
							bed_id,
							move_in_at: new Date(move_in_at),
							move_out_at: null,
							stay_type: 'permanent',
							created_by: createdBy,
							updated_by: updatedBy,
							created_at: new Date(),
							updated_at: new Date(),
						} as any,
						{ transaction: tx },
					);
				}

				// Log resident stay creation
				await AuditLogService.enqueueSystemAudit({
					requestId,
					traceId,
					actorId: residentData.created_by || null,
					actorName: residentData.created_by ? undefined : 'System',
					actorEmail: residentData.created_by ? undefined : 'system@bekind.vn',
					actorRole: residentData.created_by ? undefined : 'SYSTEM',
					action: 'RESIDENT_STAY_CREATED',
					module: 'RESIDENT',
					entityId: resident.get('id'),
					entityType: 'Resident',
					description: `Resident ${resident.get('full_name')} checked in to ${bed_ids.length} bed(s)`,
					severity: 'MEDIUM',
					payload: {
						residentId: resident.get('id'),
						residentName: resident.get('full_name'),
						bedIds: bed_ids,
						moveInAt: move_in_at,
					},
				} as any);
			}

			// Create user account if resident has phone number
			if (resident.phone) {
				const normalizedPhone = normalizePhone(resident.phone);
				// Use normalized phone if valid, otherwise use the original phone value
				const phoneForUser = normalizedPhone || resident.phone;

				const passwordHash = await PasswordService.hashPassword('Bekind@123');
				const userId = randomUUID();

				// Create user
				await User.create(
					{
						id: userId,
						email: `${phoneForUser}@bekind.com`,
						phone: phoneForUser,
						username: phoneForUser,
						first_name: resident.full_name?.split(' ')[0] || null,
						last_name: resident.full_name?.split(' ').slice(1).join(' ') || null,
						status: 'active',
						created_at: new Date(),
						updated_at: new Date(),
					},
					{ transaction: tx },
				);

				// Create user auth record
				await UserAuth.create(
					{
						id: randomUUID(),
						user_id: userId,
						password_hash: passwordHash,
						password_changed_at: new Date(),
						login_attempts: 0,
						created_at: new Date(),
						updated_at: new Date(),
					},
					{ transaction: tx },
				);

				// Assign default role
				const defaultRole = await Role.findOne({
					where: { name: UserRoleEnum.USER },
					transaction: tx,
				});

				if (!defaultRole) {
					throw new GenericError('DEFAULT_ROLE_NOT_FOUND');
				}

				await UserRoleModel.create(
					{
						id: randomUUID(),
						user_id: userId,
						role_id: defaultRole.id,
						assigned_at: new Date(),
						updated_at: new Date(),
					},
					{ transaction: tx },
				);

				// Link user to resident
				await resident.update({ user_id: userId, updated_at: new Date() }, { transaction: tx });

				// Log user account creation for resident
				await AuditLogService.enqueueSystemAudit({
					requestId,
					traceId,
					actorId: residentData.created_by || null,
					actorName: residentData.created_by ? undefined : 'System',
					actorEmail: residentData.created_by ? undefined : 'system@bekind.vn',
					actorRole: residentData.created_by ? undefined : 'SYSTEM',
					action: 'USER_ACCOUNT_CREATED',
					module: 'RESIDENT',
					entityId: userId,
					entityType: 'User',
					description: `User account created for resident ${resident.get('full_name')}`,
					severity: 'MEDIUM',
					payload: {
						userId,
						residentId: resident.get('id'),
						residentName: resident.get('full_name'),
						username: phoneForUser,
					},
				} as any);
			}

			return resident;
		}, options?.transaction);
	}

	public async getByIdWithDocuments(options: { id: string }) {
		const resident = await this.getById(options);
		if (!resident) return null;

		const documentProvider = ResidentDocumentProvider.getInstance();
		const documents = await documentProvider.getAll({
			where: { resident_id: options.id } as any,
		});

		return {
			...resident.toJSON(),
			documents: documents.rows || [],
		};
	}

	public async getByIdWithResidence(options: { id: string }) {
		const resident = await this.getById(options);
		if (!resident) return null;

		// Query resident_current_locations view directly using raw SQL
		const locations = await sequelize.query(
			'SELECT * FROM resident_current_locations WHERE resident_id = :residentId',
			{
				replacements: { residentId: options.id },
				type: QueryTypes.SELECT,
			},
		);

		const residences = locations || [];

		// Fetch user info if user_id is present
		let user = null;
		if (resident.user_id) {
			const userProvider = UserProvider.getInstance();
			const userData = await userProvider.getById({ id: resident.user_id });
			if (userData) {
				user = {
					id: userData.id,
					email: userData.email,
					username: userData.username,
					first_name: userData.first_name,
					last_name: userData.last_name,
					phone: userData.phone,
					status: userData.status,
				};
			}
		}

		return {
			...resident.toJSON(),
			user,
			residences: residences.map((residence: any) => ({
				building_id: residence.building_id,
				building_name: residence.building_name,
				building_code: residence.building_code,
				apartment_id: residence.apartment_id,
				apartment_name: residence.apartment_name,
				apartment_code: residence.apartment_code,
				apartment_floor: residence.apartment_floor,
				room_id: residence.room_id,
				room_name: residence.room_name,
				room_code: residence.room_code,
				bed_id: residence.bed_id,
				bed_name: residence.bed_name,
				bed_code: residence.bed_code,
				move_in_at: residence.move_in_at,
			})),
		};
	}

	public async getAllWithResidence(options?: any) {
		const userProvider = UserProvider.getInstance();

		// Separate filters into resident and residence filters using utility
		const residenceFields = ['room_id', 'apartment_id', 'building_id', 'bed_id'];
		const {
			targetFilters: residentFilters,
			whereClause,
			replacements,
		} = NestedFilterUtils.separateFilters(options, residenceFields, 'residence.');

		// Query resident_current_locations view with residence filters
		const locations = await sequelize.query(
			`SELECT DISTINCT resident_id FROM resident_current_locations WHERE 1=1${whereClause}`,
			{
				replacements,
				type: QueryTypes.SELECT,
			},
		);

		const filteredResidentIds = locations.map((l: any) => l.resident_id);

		// If no residents match residence filters, return empty result
		if (filteredResidentIds.length === 0 && whereClause.length > 0) {
			return {
				count: 0,
				rows: [],
				page: options?.page || 1,
				pageSize: options?.pageSize || 20,
			};
		}

		// Apply resident_id filter to resident query if residence filters exist
		const residentOptions = { ...options };
		if (whereClause.length > 0) {
			residentOptions.filters = NestedFilterUtils.rebuildFilters(residentFilters);
			residentOptions.filters = {
				...(residentOptions.filters || {}),
				id: { [Op.in]: filteredResidentIds },
			};
		} else {
			residentOptions.filters = NestedFilterUtils.rebuildFilters(residentFilters);
		}

		const residents = await this.getAll(residentOptions);
		const residentIds = residents.rows.map((r: any) => r.id);

		// Query full residence info for filtered residents
		const fullLocations =
			residentIds.length > 0
				? await sequelize.query(
						`SELECT * FROM resident_current_locations WHERE resident_id IN (${residentIds.map((_: string, i: number) => `:residentId${i}`).join(', ')})`,
						{
							replacements: Object.fromEntries(residentIds.map((id: string, i: number) => [`residentId${i}`, id])),
							type: QueryTypes.SELECT,
						},
					)
				: [];

		const locationMap = new Map(fullLocations.map((loc: any) => [loc.resident_id, loc]));

		// Fetch user info for residents that have user_id
		const userIds = residents.rows.map((r: any) => r.user_id).filter((id: string | null) => id);
		const users =
			userIds.length > 0
				? await userProvider.getAll({
						where: { id: { [Op.in]: userIds } } as any,
					})
				: { rows: [] };

		const userMap = new Map(users.rows.map((u: any) => [u.id, u]));

		const residentsWithResidence = residents.rows.map((resident: any) => {
			const location = locationMap.get(resident.id);
			const user = resident.user_id ? userMap.get(resident.user_id) : null;

			return {
				...resident.toJSON(),
				user: user
					? {
							id: user.id,
							email: user.email,
							username: user.username,
							first_name: user.first_name,
							last_name: user.last_name,
							phone: user.phone,
							status: user.status,
						}
					: null,
				residence: location
					? {
							building_id: (location as any).building_id,
							building_name: (location as any).building_name,
							building_code: (location as any).building_code,
							apartment_id: (location as any).apartment_id,
							apartment_name: (location as any).apartment_name,
							apartment_code: (location as any).apartment_code,
							apartment_floor: (location as any).apartment_floor,
							room_id: (location as any).room_id,
							room_name: (location as any).room_name,
							room_code: (location as any).room_code,
							bed_id: (location as any).bed_id,
							bed_name: (location as any).bed_name,
							bed_code: (location as any).bed_code,
							move_in_at: (location as any).move_in_at,
						}
					: null,
			};
		});

		return {
			count: residents.count,
			rows: residentsWithResidence,
			page: residents.page,
			pageSize: residents.pageSize,
		};
	}

	public async getByIdWithResidenceAndDocuments(options: { id: string }) {
		const resident = await this.getByIdWithResidence(options);
		if (!resident) return null;

		const documentProvider = ResidentDocumentProvider.getInstance();
		const documents = await documentProvider.getAll({
			where: { resident_id: options.id } as any,
		});

		return {
			...resident,
			documents: documents.rows || [],
		};
	}

	public async getRoommates(options: { id: string }) {
		// First, get the current resident's room
		const [currentLocation] = await sequelize.query(
			'SELECT room_id FROM resident_current_locations WHERE resident_id = :residentId LIMIT 1',
			{
				replacements: { residentId: options.id },
				type: QueryTypes.SELECT,
			},
		);

		if (!currentLocation) {
			return {
				count: 0,
				rows: [],
			};
		}

		const roomId = (currentLocation as any).room_id;

		// Get all residents in the same room (excluding the current resident)
		const roommates = await sequelize.query(
			`SELECT
				rcl.resident_id,
				rcl.resident_name AS full_name,
				r.phone,
				rcl.bed_id,
				rcl.bed_name,
				rcl.bed_code,
				rcl.room_id,
				rcl.room_name,
				rcl.room_code
			FROM resident_current_locations rcl
			JOIN residents r ON r.id = rcl.resident_id
			WHERE rcl.room_id = :roomId
				AND rcl.resident_id != :residentId
				AND r.deleted_at IS NULL`,
			{
				replacements: { roomId, residentId: options.id },
				type: QueryTypes.SELECT,
			},
		);

		return {
			count: roommates.length,
			rows: roommates,
		};
	}

	protected override getUniqueRestoreConstraints(): string[] {
		return ['email', 'id_card'];
	}
}
