import {
    ConflictException,
    Injectable,
    Logger,
    NotFoundException,
    UnauthorizedException,
    BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, In, QueryFailedError, Repository } from 'typeorm';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { RegisterDto } from './dto/register.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity';
import { Children, StatutEnfantType } from 'src/entities/children.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
import { StatutDossierType } from 'src/entities/dossiers.entity';
import { LoginDto } from './dto/login.dto';
import { RepriseDossierDto } from './dto/reprise-dossier.dto';
import { ResoumettreRepriseDto } from './dto/resoumettre-reprise.dto';
import { RepriseIdentifyResponseDto } from './dto/reprise-identify.dto';
import { AppConfigService } from 'src/modules/config/config.service';
import { validateNir } from 'src/common/utils/nir.util';
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
import { MailService } from 'src/modules/mail/mail.service';
import { ParentsService } from '../parents/parents.service';
import { DossiersService } from '../dossiers/dossiers.service';
import { DossierAmCompletDto } from '../dossiers/dto/dossier-am-complet.dto';

@Injectable()
export class AuthService {
    private readonly logger = new Logger(AuthService.name);

    constructor(
        private readonly usersService: UserService,
        private readonly jwtService: JwtService,
        private readonly configService: ConfigService,
        private readonly appConfigService: AppConfigService,
        private readonly mailService: MailService,
        private readonly numeroDossierService: NumeroDossierService,
        private readonly parentsService: ParentsService,
        private readonly dossiersService: DossiersService,
        @InjectRepository(Parents)
        private readonly parentsRepo: Repository<Parents>,
        @InjectRepository(Users)
        private readonly usersRepo: Repository<Users>,
        @InjectRepository(Children)
        private readonly childrenRepo: Repository<Children>,
        @InjectRepository(AssistanteMaternelle)
        private readonly assistantesMaternellesRepo: Repository<AssistanteMaternelle>,
    ) { }

    /**
     * Génère un access_token et un refresh_token
     */
    async generateTokens(userId: string, email: string, role: RoleType) {
        const accessSecret = this.configService.get<string>('jwt.accessSecret');
        const accessExpiresIn = this.configService.get<string>('jwt.accessExpiresIn');
        const refreshSecret = this.configService.get<string>('jwt.refreshSecret');
        const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn');

        const [accessToken, refreshToken] = await Promise.all([
            this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }),
            this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }),
        ]);

        return {
            access_token: accessToken,
            refresh_token: refreshToken,
        };
    }

    /**
     * Connexion utilisateur
     */
    async login(dto: LoginDto) {
        const user = await this.usersService.findByEmailOrNull(dto.email);

        if (!user) {
            throw new UnauthorizedException('Identifiants invalides');
        }

        // Vérifier que le mot de passe existe (compte activé)
        if (!user.password) {
            throw new UnauthorizedException(
                'Compte non activé. Veuillez créer votre mot de passe via le lien reçu par email.',
            );
        }

        // Vérifier le mot de passe
        const isMatch = await bcrypt.compare(dto.password, user.password);
        if (!isMatch) {
            throw new UnauthorizedException('Identifiants invalides');
        }

        // Vérifier le statut du compte
        if (user.statut === StatutUtilisateurType.EN_ATTENTE) {
            throw new UnauthorizedException(
                'Votre compte est en attente de validation par un gestionnaire.',
            );
        }

        if (user.statut === StatutUtilisateurType.SUSPENDU) {
            throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.');
        }

        if (user.statut === StatutUtilisateurType.REFUSE) {
            throw new UnauthorizedException(
                'Votre compte a été refusé. Vous pouvez corriger votre dossier et le soumettre à nouveau ; un gestionnaire pourra le réexaminer.',
            );
        }

        return this.generateTokens(user.id, user.email, user.role);
    }

    /**
     * Rafraîchir les tokens
     */
    async refreshTokens(refreshToken: string) {
        try {
            const payload = await this.jwtService.verifyAsync(refreshToken, {
                secret: this.configService.get<string>('jwt.refreshSecret'),
            });

            const user = await this.usersService.findOne(payload.sub);
            if (!user) {
                throw new UnauthorizedException('Utilisateur introuvable');
            }

            return this.generateTokens(user.id, user.email, user.role);
        } catch {
            throw new UnauthorizedException('Refresh token invalide');
        }
    }

    async verifyCreatePasswordToken(token: string) {
        const cleanedToken = token?.trim();
        if (!cleanedToken) {
            // #118: côté front, un token absent doit être traité comme lien invalide/expiré.
            // On renvoie donc la même réponse neutre 404 que pour les autres cas invalides.
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        const user = await this.usersRepo.findOne({
            where: { token_creation_mdp: cleanedToken },
            select: ['id', 'token_creation_mdp_expire_le', 'password'],
        });

        if (!user) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        if (user.password) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        if (!user.token_creation_mdp_expire_le || user.token_creation_mdp_expire_le <= new Date()) {
            // Invalidation explicite d'un token expiré pour éviter toute réutilisation ultérieure.
            await this.usersRepo
                .createQueryBuilder()
                .update(Users)
                .set({
                    token_creation_mdp: () => 'NULL',
                    token_creation_mdp_expire_le: () => 'NULL',
                })
                .where('id = :id', { id: user.id })
                .execute();
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        return {
            valid: true,
            message: 'Token valide',
        };
    }

    async createPasswordWithToken(token: string, plainPassword: string) {
        const cleanedToken = token?.trim();
        if (!cleanedToken) {
            throw new BadRequestException('Token manquant');
        }

        const existingUser = await this.usersRepo.findOne({
            where: {
                token_creation_mdp: cleanedToken,
            },
            select: ['id', 'password', 'token_creation_mdp_expire_le'],
        });

        if (!existingUser) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        if (existingUser.password) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        if (
            !existingUser.token_creation_mdp_expire_le ||
            existingUser.token_creation_mdp_expire_le <= new Date()
        ) {
            await this.usersRepo
                .createQueryBuilder()
                .update(Users)
                .set({
                    token_creation_mdp: () => 'NULL',
                    token_creation_mdp_expire_le: () => 'NULL',
                })
                .where('id = :id', { id: existingUser.id })
                .execute();
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        const sel = await bcrypt.genSalt(12);
        const hashedPassword = await bcrypt.hash(plainPassword, sel);

        // Usage unique atomique : une seule requête UPDATE avec garde-fous.
        const updateResult = await this.usersRepo
            .createQueryBuilder()
            .update(Users)
            .set({
                password: hashedPassword,
                token_creation_mdp: () => 'NULL',
                token_creation_mdp_expire_le: () => 'NULL',
                statut: StatutUtilisateurType.ACTIF,
                changement_mdp_obligatoire: false,
            })
            .where('token_creation_mdp = :token', { token: cleanedToken })
            .andWhere('token_creation_mdp_expire_le > NOW()')
            .andWhere('password IS NULL')
            .execute();

        if (!updateResult.affected) {
            this.logger.warn(
                '[createPasswordWithToken] Token non consommé (course concurrente ou token invalidé)',
            );
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        this.logger.log('[createPasswordWithToken] Mot de passe initial créé avec succès');

        return {
            message: 'Mot de passe créé avec succès. Vous pouvez maintenant vous connecter.',
            userId: existingUser.id,
        };
    }

    /** Réponse unique pour POST forgot-password (anti-énumération), ticket #127 */
    private static readonly FORGOT_PASSWORD_GENERIC_MESSAGE =
        'Si cette adresse est associée à un compte pour lequel un mot de passe existe, vous recevrez un e-mail avec les instructions.';

    /**
     * Ticket #127 — demande de réinitialisation (e-mail avec lien /reset-password).
     * Toujours la même réponse 200 ; n’utilise pas token_creation_mdp (#118 inscription).
     */
    async requestPasswordReset(rawEmail: string): Promise<{ message: string }> {
        const generic = { message: AuthService.FORGOT_PASSWORD_GENERIC_MESSAGE };
        const email = (rawEmail ?? '').trim().toLowerCase();
        if (!email) {
            return generic;
        }

        const user = await this.usersRepo
            .createQueryBuilder('u')
            .where('LOWER(TRIM(u.email)) = :email', { email })
            .select(['u.id', 'u.email', 'u.prenom', 'u.nom', 'u.password'])
            .getOne();

        if (!user?.password) {
            return generic;
        }

        const expiryDaysRaw = this.appConfigService.get<number>('password_reset_token_expiry_days', 7);
        const expiryDays = Number(expiryDaysRaw) > 0 ? Number(expiryDaysRaw) : 7;
        const expires = new Date();
        expires.setDate(expires.getDate() + expiryDays);

        const token = crypto.randomUUID();

        await this.usersRepo.update(
            { id: user.id },
            {
                password_reset_token: token,
                password_reset_expires: expires,
            },
        );

        try {
            await this.mailService.sendPasswordResetEmail({
                email: user.email,
                prenom: user.prenom ?? '',
                nom: user.nom ?? '',
                token,
            });
        } catch (err) {
            this.logger.error(
                '[requestPasswordReset] Échec envoi e-mail (réponse inchangée pour anti-énumération)',
                err,
            );
        }

        return generic;
    }

    /**
     * Ticket #127 — consomme password_reset_token (distinct de create-password / token_creation_mdp).
     */
    async resetPasswordWithResetToken(token: string, plainPassword: string): Promise<{ message: string }> {
        const cleanedToken = token?.trim();
        if (!cleanedToken) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        const existingUser = await this.usersRepo.findOne({
            where: { password_reset_token: cleanedToken },
            select: ['id', 'password', 'password_reset_expires'],
        });

        if (!existingUser?.password) {
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        if (!existingUser.password_reset_expires || existingUser.password_reset_expires <= new Date()) {
            await this.usersRepo
                .createQueryBuilder()
                .update(Users)
                .set({
                    password_reset_token: () => 'NULL',
                    password_reset_expires: () => 'NULL',
                })
                .where('id = :id', { id: existingUser.id })
                .execute();
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        const sel = await bcrypt.genSalt(12);
        const hashedPassword = await bcrypt.hash(plainPassword, sel);

        const updateResult = await this.usersRepo
            .createQueryBuilder()
            .update(Users)
            .set({
                password: hashedPassword,
                password_reset_token: () => 'NULL',
                password_reset_expires: () => 'NULL',
                changement_mdp_obligatoire: false,
            })
            .where('password_reset_token = :token', { token: cleanedToken })
            .andWhere('password_reset_expires > NOW()')
            .execute();

        if (!updateResult.affected) {
            this.logger.warn(
                '[resetPasswordWithResetToken] Token non consommé (course concurrente ou token invalidé)',
            );
            throw new NotFoundException('Token invalide, expiré, ou déjà utilisé.');
        }

        this.logger.log('[resetPasswordWithResetToken] Mot de passe réinitialisé avec succès');

        return {
            message: 'Mot de passe mis à jour. Vous pouvez vous connecter.',
        };
    }

    /**
     * Inscription utilisateur OBSOLÈTE - Utiliser inscrireParentComplet() ou registerAM()
     * @deprecated
     */
    async register(registerDto: RegisterDto) {
        const exists = await this.usersService.findByEmailOrNull(registerDto.email);
        if (exists) {
            throw new ConflictException('Email déjà utilisé');
        }

        const allowedRoles = new Set<RoleType>([RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE]);
        if (!allowedRoles.has(registerDto.role)) {
            registerDto.role = RoleType.PARENT;
        }

        registerDto.statut = StatutUtilisateurType.EN_ATTENTE;

        if (!registerDto.consentement_photo) {
            registerDto.date_consentement_photo = null;
        } else if (registerDto.date_consentement_photo) {
            const date = new Date(registerDto.date_consentement_photo);
            if (isNaN(date.getTime())) {
                registerDto.date_consentement_photo = null;
            }
        }

        const user = await this.usersService.createUser(registerDto);
        const tokens = await this.generateTokens(user.id, user.email, user.role);

        return {
            ...tokens,
            user: {
                id: user.id,
                email: user.email,
                role: user.role,
                prenom: user.prenom,
                nom: user.nom,
                statut: user.statut,
            },
        };
    }

    /**
     * Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
     * Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
     */
    async inscrireParentComplet(dto: RegisterParentCompletDto) {
        if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
            throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire');
        }

        if (!dto.enfants || dto.enfants.length === 0) {
            throw new BadRequestException('Au moins un enfant est requis');
        }

        const existe = await this.usersService.findByEmailOrNull(dto.email);
        if (existe) {
            throw new ConflictException('Un compte avec cet email existe déjà');
        }

        if (dto.co_parent_email) {
            if (dto.email.trim().toLowerCase() === dto.co_parent_email.trim().toLowerCase()) {
                throw new BadRequestException(
                    'L\'email du parent et du co-parent doivent être différents.',
                );
            }
            const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
            if (coParentExiste) {
                throw new ConflictException('L\'email du co-parent est déjà utilisé');
            }
        }

        const joursExpirationToken = await this.appConfigService.get<number>(
            'password_reset_token_expiry_days',
            7,
        );

        const tokenCreationMdp = crypto.randomUUID();
        const dateExpiration = new Date();
        dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);

        let resultat: {
            parent1: Users;
            parent2: Users | null;
            enfants: Children[];
            tokenCreationMdp: string;
            tokenCoParent: string | null;
        };

        try {
            resultat = await this.usersRepo.manager.transaction(async (manager) => {
            const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);

            const parent1 = manager.create(Users, {
                email: dto.email,
                prenom: dto.prenom,
                nom: dto.nom,
                role: RoleType.PARENT,
                statut: StatutUtilisateurType.EN_ATTENTE,
                telephone: dto.telephone,
                adresse: dto.adresse,
                code_postal: dto.code_postal,
                ville: dto.ville,
                token_creation_mdp: tokenCreationMdp,
                token_creation_mdp_expire_le: dateExpiration,
                numero_dossier: numeroDossier,
            });

            const parent1Enregistre = await manager.save(Users, parent1);

            let parent2Enregistre: Users | null = null;
            let tokenCoParent: string | null = null;

            if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
                tokenCoParent = crypto.randomUUID();
                const dateExpirationCoParent = new Date();
                dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken);

                const parent2 = manager.create(Users, {
                    email: dto.co_parent_email,
                    prenom: dto.co_parent_prenom,
                    nom: dto.co_parent_nom,
                    role: RoleType.PARENT,
                    statut: StatutUtilisateurType.EN_ATTENTE,
                    telephone: dto.co_parent_telephone,
                    adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
                    code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
                    ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
                    token_creation_mdp: tokenCoParent,
                    token_creation_mdp_expire_le: dateExpirationCoParent,
                    numero_dossier: numeroDossier,
                });

                parent2Enregistre = await manager.save(Users, parent2);
            }

            const entiteParent = manager.create(Parents, {
                user_id: parent1Enregistre.id,
                numero_dossier: numeroDossier,
            });
            entiteParent.user = parent1Enregistre;
            if (parent2Enregistre) {
                entiteParent.co_parent = parent2Enregistre;
            }

            await manager.save(Parents, entiteParent);

            if (parent2Enregistre) {
                const entiteCoParent = manager.create(Parents, {
                    user_id: parent2Enregistre.id,
                    numero_dossier: numeroDossier,
                });
                entiteCoParent.user = parent2Enregistre;
                entiteCoParent.co_parent = parent1Enregistre;

                await manager.save(Parents, entiteCoParent);
            }

            const enfantsEnregistres: Children[] = [];
            for (const enfantDto of dto.enfants) {
                let urlPhoto: string | null = null;

                if (enfantDto.photo_base64 && enfantDto.photo_filename) {
                    urlPhoto = await this.sauvegarderPhotoDepuisBase64(
                        enfantDto.photo_base64,
                        enfantDto.photo_filename,
                    );
                }

                const enfant = new Children();
                enfant.first_name = enfantDto.prenom;
                enfant.last_name = enfantDto.nom || dto.nom;
                enfant.gender = enfantDto.genre;
                enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined;
                enfant.due_date = enfantDto.date_previsionnelle_naissance 
                    ? new Date(enfantDto.date_previsionnelle_naissance) 
                    : undefined;
                enfant.photo_url = urlPhoto || undefined;
                enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE;
                enfant.consent_photo = !!enfantDto.consent_photo;
                enfant.consent_photo_at = enfant.consent_photo ? new Date() : null!;
                enfant.is_multiple = enfantDto.grossesse_multiple || false;

                const enfantEnregistre = await manager.save(Children, enfant);
                enfantsEnregistres.push(enfantEnregistre);

                const lienParentEnfant1 = manager.create(ParentsChildren, {
                    parentId: parent1Enregistre.id,
                    enfantId: enfantEnregistre.id,
                });
                await manager.save(ParentsChildren, lienParentEnfant1);

                if (parent2Enregistre) {
                    const lienParentEnfant2 = manager.create(ParentsChildren, {
                        parentId: parent2Enregistre.id,
                        enfantId: enfantEnregistre.id,
                    });
                    await manager.save(ParentsChildren, lienParentEnfant2);
                }
            }

            // Dossier famille : motivation (texte_motivation côté GET) + liaisons enfants (ticket #119)
            const presentationTrim = dto.presentation_dossier?.trim();
            const dossierFamilleEnt = manager.create(DossierFamille, {
                numero_dossier: numeroDossier,
                presentation: presentationTrim || undefined,
                statut: StatutDossierType.ENVOYE,
                parent: entiteParent,
            });
            const dossierFamilleSaved = await manager.save(DossierFamille, dossierFamilleEnt);
            for (const enfantEnregistre of enfantsEnregistres) {
                await manager.save(
                    DossierFamilleEnfant,
                    manager.create(DossierFamilleEnfant, {
                        id_dossier_famille: dossierFamilleSaved.id,
                        id_enfant: enfantEnregistre.id,
                    }),
                );
            }

            return {
                parent1: parent1Enregistre,
                parent2: parent2Enregistre,
                enfants: enfantsEnregistres,
                tokenCreationMdp,
                tokenCoParent,
            };
        });
        } catch (err) {
            if (this.isPostgresUniqueViolation(err)) {
                throw new ConflictException(
                    'Un compte avec cet email existe déjà (contrainte unique en base).',
                );
            }
            throw err;
        }

        const numeroDossier = resultat.parent1.numero_dossier ?? '';

        try {
            await this.mailService.sendRegistrationPendingEmail(
                resultat.parent1.email,
                resultat.parent1.prenom ?? '',
                resultat.parent1.nom ?? '',
                numeroDossier,
            );
            if (resultat.parent2) {
                await this.mailService.sendRegistrationPendingEmail(
                    resultat.parent2.email,
                    resultat.parent2.prenom ?? '',
                    resultat.parent2.nom ?? '',
                    numeroDossier,
                );
            }
        } catch (err) {
            this.logger.error(
                "[inscrireParentComplet] Échec envoi email d'accusé de réception (inscription conservée)",
                err instanceof Error ? err.stack : String(err),
            );
        }

        return {
            message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
            parent_id: resultat.parent1.id,
            co_parent_id: resultat.parent2?.id,
            enfants_ids: resultat.enfants.map(e => e.id),
            statut: StatutUtilisateurType.EN_ATTENTE,
            numero_dossier: numeroDossier,
        };
    }

    /**
     * Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
     * Crée User (role AM) + entrée assistantes_maternelles, token création MDP
     */
    async inscrireAMComplet(dto: RegisterAMCompletDto) {
        if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
            throw new BadRequestException(
                "L'acceptation des CGU et de la politique de confidentialité est obligatoire",
            );
        }

        if (dto.places_disponibles > dto.capacite_accueil) {
            throw new BadRequestException(
                'Le nombre de places disponibles ne peut pas dépasser la capacité d\'accueil.',
            );
        }

        const nirNormalized = (dto.nir || '').replace(/\s/g, '').toUpperCase();
        const nirValidation = validateNir(nirNormalized, {
            dateNaissance: dto.date_naissance,
        });
        if (!nirValidation.valid) {
            throw new BadRequestException(nirValidation.error || 'NIR invalide');
        }
        if (nirValidation.warning) {
            // Warning uniquement : on ne bloque pas (AM souvent étrangères, DOM-TOM, Corse)
            console.warn('[inscrireAMComplet] NIR warning:', nirValidation.warning, 'email=', dto.email);
        }

        const existe = await this.usersService.findByEmailOrNull(dto.email);
        if (existe) {
            throw new ConflictException('Un compte avec cet email existe déjà');
        }

        const nirDejaUtilise = await this.assistantesMaternellesRepo.findOne({
            where: { nir: nirNormalized },
        });
        if (nirDejaUtilise) {
            throw new ConflictException(
                'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
            );
        }

        const numeroAgrement = (dto.numero_agrement || '').trim();
        if (numeroAgrement) {
            const agrementDejaUtilise = await this.assistantesMaternellesRepo.findOne({
                where: { approval_number: numeroAgrement },
            });
            if (agrementDejaUtilise) {
                throw new ConflictException(
                    'Un compte assistante maternelle avec ce numéro d\'agrément existe déjà.',
                );
            }
        }

        const joursExpirationToken = await this.appConfigService.get<number>(
            'password_reset_token_expiry_days',
            7,
        );
        const tokenCreationMdp = crypto.randomUUID();
        const dateExpiration = new Date();
        dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);

        let urlPhoto: string | null = null;
        const photoB64 = (dto.photo_base64 ?? '').trim();
        if (photoB64) {
            const nomFichier =
                (dto.photo_filename ?? '').trim() || 'photo_am.jpg';
            urlPhoto = await this.sauvegarderPhotoDepuisBase64(photoB64, nomFichier);
        }

        const dateConsentementPhoto =
            dto.consentement_photo ? new Date() : undefined;

        let resultat: { user: Users };
        try {
            resultat = await this.usersRepo.manager.transaction(async (manager) => {
            const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);

            const user = manager.create(Users, {
                email: dto.email,
                prenom: dto.prenom,
                nom: dto.nom,
                role: RoleType.ASSISTANTE_MATERNELLE,
                statut: StatutUtilisateurType.EN_ATTENTE,
                telephone: dto.telephone,
                adresse: dto.adresse,
                code_postal: dto.code_postal,
                ville: dto.ville,
                token_creation_mdp: tokenCreationMdp,
                token_creation_mdp_expire_le: dateExpiration,
                photo_url: urlPhoto ?? undefined,
                consentement_photo: dto.consentement_photo,
                date_consentement_photo: dateConsentementPhoto,
                date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
                lieu_naissance_ville: dto.lieu_naissance_ville,
                lieu_naissance_pays: dto.lieu_naissance_pays,
                numero_dossier: numeroDossier,
            });
            const userEnregistre = await manager.save(Users, user);

            const amRepo = manager.getRepository(AssistanteMaternelle);
            const am = amRepo.create({
                user_id: userEnregistre.id,
                approval_number: dto.numero_agrement,
                nir: nirNormalized,
                max_children: dto.capacite_accueil,
                places_available: dto.places_disponibles,
                biography: dto.biographie,
                residence_city: dto.ville ?? undefined,
                agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
                available: true,
                numero_dossier: numeroDossier,
            });
            await amRepo.save(am);

            return { user: userEnregistre };
        });
        } catch (err) {
            if (this.isPostgresUniqueViolation(err)) {
                throw new ConflictException('Un compte avec cet email existe déjà (contrainte unique en base).');
            }
            throw err;
        }

        const numeroDossier = resultat.user.numero_dossier ?? '';

        try {
            await this.mailService.sendRegistrationPendingEmail(
                resultat.user.email,
                resultat.user.prenom ?? '',
                resultat.user.nom ?? '',
                numeroDossier,
            );
        } catch (err) {
            this.logger.error(
                "[inscrireAMComplet] Échec envoi email d'accusé de réception (inscription conservée)",
                err instanceof Error ? err.stack : String(err),
            );
        }

        return {
            message:
                'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
            user_id: resultat.user.id,
            statut: StatutUtilisateurType.EN_ATTENTE,
            numero_dossier: numeroDossier,
        };
    }

    /**
     * Sauvegarde une photo depuis base64 vers le système de fichiers
     */
    private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise<string> {
        const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/);
        if (!correspondances) {
            throw new BadRequestException('Format de photo invalide (doit être base64)');
        }

        const extension = correspondances[1];
        const tamponImage = Buffer.from(correspondances[2], 'base64');

        const rawDir = process.env.UPLOAD_PHOTOS_DIR?.trim();
        const dossierUpload = rawDir
            ? path.isAbsolute(rawDir)
                ? rawDir
                : path.resolve(process.cwd(), rawDir)
            : path.join(process.cwd(), 'uploads', 'photos');
        await fs.mkdir(dossierUpload, { recursive: true });

        const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
        const cheminFichier = path.join(dossierUpload, nomFichierUnique);

        await fs.writeFile(cheminFichier, tamponImage);

        return `/uploads/photos/${nomFichierUnique}`;
    }

    /**
     * Changement de mot de passe obligatoire (première connexion)
     */
    async changePasswordRequired(
        userId: string,
        motDePasseActuel: string,
        nouveauMotDePasse: string,
    ) {
        const user = await this.usersRepo.findOne({ where: { id: userId } });

        if (!user) {
            throw new UnauthorizedException('Utilisateur introuvable');
        }

        // Vérifier que le changement est bien obligatoire
        if (!user.changement_mdp_obligatoire) {
            throw new BadRequestException(
                'Le changement de mot de passe n\'est pas requis pour cet utilisateur',
            );
        }

        // Vérifier que l'utilisateur a un mot de passe
        if (!user.password) {
            throw new BadRequestException('Compte non activé');
        }

        // Vérifier le mot de passe actuel
        const motDePasseValide = await bcrypt.compare(motDePasseActuel, user.password);
        if (!motDePasseValide) {
            throw new BadRequestException('Mot de passe actuel incorrect');
        }

        // Vérifier que le nouveau mot de passe est différent de l'ancien
        const memeMotDePasse = await bcrypt.compare(nouveauMotDePasse, user.password);
        if (memeMotDePasse) {
            throw new BadRequestException(
                'Le nouveau mot de passe doit être différent de l\'ancien',
            );
        }

        // Hasher et sauvegarder le nouveau mot de passe
        const sel = await bcrypt.genSalt(12);
        user.password = await bcrypt.hash(nouveauMotDePasse, sel);
        user.changement_mdp_obligatoire = false;
        user.modifie_le = new Date();

        await this.usersRepo.save(user);

        return {
            success: true,
            message: 'Mot de passe changé avec succès',
        };
    }

    async logout(userId: string) {
        return { success: true, message: 'Deconnexion'}
    }

    /** GET dossier reprise – token seul. Ticket #111, enrichi #112 */
    async getRepriseDossier(token: string): Promise<RepriseDossierDto> {
        const user = await this.usersService.findByTokenReprise(token);
        if (!user) {
            throw new NotFoundException('Token reprise invalide ou expiré.');
        }

        const base = this.userToRepriseBase(user);

        if (user.role === RoleType.PARENT && user.numero_dossier) {
            try {
                const famille = await this.parentsService.getDossierFamilleByNumero(user.numero_dossier);
                return {
                    ...base,
                    parents: famille.parents,
                    enfants: famille.enfants,
                    texte_motivation: famille.texte_motivation,
                };
            } catch (err) {
                if (err instanceof NotFoundException) {
                    return base;
                }
                throw err;
            }
        }

        if (user.role === RoleType.ASSISTANTE_MATERNELLE && user.numero_dossier) {
            try {
                const unifie = await this.dossiersService.getDossierByNumero(user.numero_dossier);
                if (unifie.type === 'am') {
                    return this.mergeAmRepriseDossier(base, unifie.dossier as DossierAmCompletDto);
                }
            } catch (err) {
                if (err instanceof NotFoundException) {
                    return base;
                }
                throw err;
            }
        }

        return base;
    }

    /** PATCH resoumission reprise – dossier complet. Ticket #111, enrichi #112 */
    async resoumettreReprise(dto: ResoumettreRepriseDto): Promise<{
        message: string;
        statut: StatutUtilisateurType;
        user_id: string;
        numero_dossier?: string;
    }> {
        const user = await this.usersService.findByTokenReprise(dto.token);
        if (!user) {
            throw new NotFoundException('Token reprise invalide ou expiré.');
        }

        if (user.role === RoleType.PARENT) {
            return this.resoumettreRepriseParent(user, dto);
        }
        if (user.role === RoleType.ASSISTANTE_MATERNELLE) {
            return this.resoumettreRepriseAM(user, dto);
        }

        throw new BadRequestException('Rôle non pris en charge pour la reprise.');
    }

    private userToRepriseBase(user: Users): RepriseDossierDto {
        return {
            id: user.id,
            email: user.email,
            prenom: user.prenom,
            nom: user.nom,
            telephone: user.telephone,
            adresse: user.adresse,
            ville: user.ville,
            code_postal: user.code_postal,
            numero_dossier: user.numero_dossier,
            role: user.role,
            photo_url: user.photo_url,
            genre: user.genre,
            situation_familiale: user.situation_familiale,
        };
    }

    private mergeAmRepriseDossier(base: RepriseDossierDto, dossier: DossierAmCompletDto): RepriseDossierDto {
        const u = dossier.user;
        return {
            ...base,
            email: u.email,
            prenom: u.prenom,
            nom: u.nom,
            telephone: u.telephone,
            adresse: u.adresse,
            ville: u.ville,
            code_postal: u.code_postal,
            photo_url: u.photo_url,
            consentement_photo: u.consentement_photo,
            date_naissance: u.date_naissance,
            lieu_naissance_ville: u.lieu_naissance_ville,
            lieu_naissance_pays: u.lieu_naissance_pays,
            numero_agrement: dossier.numero_agrement,
            nir: dossier.nir,
            date_agrement: dossier.date_agrement,
            nb_max_enfants: dossier.nb_max_enfants,
            place_disponible: dossier.place_disponible,
            biographie: dossier.biographie,
        };
    }

    private applyTitulaireUserFields(user: Users, dto: ResoumettreRepriseDto): void {
        if (dto.prenom !== undefined) user.prenom = dto.prenom;
        if (dto.nom !== undefined) user.nom = dto.nom;
        if (dto.telephone !== undefined) user.telephone = dto.telephone;
        if (dto.adresse !== undefined) user.adresse = dto.adresse;
        if (dto.ville !== undefined) user.ville = dto.ville;
        if (dto.code_postal !== undefined) user.code_postal = dto.code_postal;
    }

    private async resoumettreRepriseParent(
        titulaire: Users,
        dto: ResoumettreRepriseDto,
    ): Promise<{ message: string; statut: StatutUtilisateurType; user_id: string; numero_dossier?: string }> {
        const motivation =
            dto.texte_motivation?.trim() ??
            dto.presentation_dossier?.trim();

        const result = await this.usersRepo.manager.transaction(async (manager) => {
            const familyUsers = titulaire.numero_dossier
                ? await manager.find(Users, {
                    where: {
                        role: RoleType.PARENT,
                        numero_dossier: titulaire.numero_dossier,
                    },
                })
                : [titulaire];

            const titulaireLive = familyUsers.find((u) => u.id === titulaire.id) ?? titulaire;
            this.applyTitulaireUserFields(titulaireLive, dto);

            const coParentUser = familyUsers.find((u) => u.id !== titulaire.id);
            if (coParentUser) {
                if (dto.co_parent_prenom !== undefined) coParentUser.prenom = dto.co_parent_prenom;
                if (dto.co_parent_nom !== undefined) coParentUser.nom = dto.co_parent_nom;
                if (dto.co_parent_telephone !== undefined) coParentUser.telephone = dto.co_parent_telephone;

                if (dto.co_parent_meme_adresse) {
                    coParentUser.adresse = titulaireLive.adresse;
                    coParentUser.code_postal = titulaireLive.code_postal;
                    coParentUser.ville = titulaireLive.ville;
                } else {
                    if (dto.co_parent_adresse !== undefined) coParentUser.adresse = dto.co_parent_adresse;
                    if (dto.co_parent_code_postal !== undefined) coParentUser.code_postal = dto.co_parent_code_postal;
                    if (dto.co_parent_ville !== undefined) coParentUser.ville = dto.co_parent_ville;
                }
            }

            if (titulaire.numero_dossier && motivation !== undefined) {
                const dossierFamille = await manager.findOne(DossierFamille, {
                    where: { numero_dossier: titulaire.numero_dossier },
                });
                if (dossierFamille) {
                    dossierFamille.presentation = motivation || undefined;
                    await manager.save(DossierFamille, dossierFamille);
                }
            }

            if (dto.enfants?.length) {
                const familyChildIds = await this.getFamilyChildrenIds(manager, titulaireLive.id);
                for (const enfantDto of dto.enfants) {
                    if (!familyChildIds.has(enfantDto.id)) {
                        throw new BadRequestException(
                            `Enfant inconnu pour ce dossier : ${enfantDto.id}`,
                        );
                    }

                    const enfant = await manager.findOne(Children, { where: { id: enfantDto.id } });
                    if (!enfant) {
                        throw new BadRequestException(`Enfant introuvable : ${enfantDto.id}`);
                    }

                    if (enfantDto.prenom !== undefined) enfant.first_name = enfantDto.prenom;
                    if (enfantDto.nom !== undefined) enfant.last_name = enfantDto.nom;
                    if (enfantDto.genre !== undefined) enfant.gender = enfantDto.genre;
                    if (enfantDto.date_naissance !== undefined) {
                        enfant.birth_date = new Date(enfantDto.date_naissance);
                        enfant.status = StatutEnfantType.SANS_GARDE;
                    }
                    if (enfantDto.date_previsionnelle_naissance !== undefined) {
                        enfant.due_date = new Date(enfantDto.date_previsionnelle_naissance);
                        if (!enfantDto.date_naissance) {
                            enfant.status = StatutEnfantType.A_NAITRE;
                        }
                    }
                    if (enfantDto.grossesse_multiple !== undefined) {
                        enfant.is_multiple = enfantDto.grossesse_multiple;
                    }
                    if (enfantDto.consent_photo !== undefined) {
                        enfant.consent_photo = !!enfantDto.consent_photo;
                        enfant.consent_photo_at = enfant.consent_photo
                            ? new Date()
                            : null!;
                    }

                    if (enfantDto.photo_base64 && enfantDto.photo_filename) {
                        enfant.photo_url = await this.sauvegarderPhotoDepuisBase64(
                            enfantDto.photo_base64,
                            enfantDto.photo_filename,
                        );
                    }

                    await manager.save(Children, enfant);
                }
            }

            for (const familyUser of familyUsers) {
                familyUser.statut = StatutUtilisateurType.EN_ATTENTE;
                familyUser.token_reprise = undefined;
                familyUser.token_reprise_expire_le = undefined;
                await manager.save(Users, familyUser);
            }

            return {
                message: 'Dossier resoumis avec succès. Il est de nouveau en attente de validation.',
                statut: StatutUtilisateurType.EN_ATTENTE,
                user_id: titulaireLive.id,
                numero_dossier: titulaireLive.numero_dossier,
                parentsPourEmail: familyUsers,
            };
        });

        const numeroDossier = result.numero_dossier ?? '';
        try {
            for (const parent of result.parentsPourEmail) {
                await this.mailService.sendResoumissionPendingEmail(
                    parent.email,
                    parent.prenom ?? '',
                    parent.nom ?? '',
                    numeroDossier,
                );
            }
        } catch (err) {
            this.logger.error(
                "[resoumettreRepriseParent] Échec envoi email accusé resoumission (dossier conservé)",
                err instanceof Error ? err.stack : String(err),
            );
        }

        const { parentsPourEmail: _parentsPourEmail, ...response } = result;
        return response;
    }

    private async resoumettreRepriseAM(
        user: Users,
        dto: ResoumettreRepriseDto,
    ): Promise<{ message: string; statut: StatutUtilisateurType; user_id: string; numero_dossier?: string }> {
        if (
            dto.capacite_accueil !== undefined &&
            dto.places_disponibles !== undefined &&
            dto.places_disponibles > dto.capacite_accueil
        ) {
            throw new BadRequestException(
                'Le nombre de places disponibles ne peut pas dépasser la capacité d\'accueil.',
            );
        }

        let urlPhoto: string | undefined;
        const photoB64 = (dto.photo_base64 ?? '').trim();
        if (photoB64) {
            const nomFichier = (dto.photo_filename ?? '').trim() || 'photo_am.jpg';
            urlPhoto = await this.sauvegarderPhotoDepuisBase64(photoB64, nomFichier);
        } else if (dto.photo_url !== undefined) {
            urlPhoto = dto.photo_url;
        }

        if (dto.nir !== undefined) {
            const nirNormalized = dto.nir.replace(/\s/g, '').toUpperCase();
            const nirValidation = validateNir(nirNormalized, {
                dateNaissance: dto.date_naissance ?? user.date_naissance?.toISOString().slice(0, 10),
            });
            if (!nirValidation.valid) {
                throw new BadRequestException(nirValidation.error || 'NIR invalide');
            }
        }

        return this.usersRepo.manager.transaction(async (manager) => {
            const userLive = await manager.findOne(Users, { where: { id: user.id } });
            if (!userLive) {
                throw new NotFoundException('Utilisateur introuvable.');
            }

            this.applyTitulaireUserFields(userLive, dto);
            if (urlPhoto !== undefined) userLive.photo_url = urlPhoto;
            if (dto.consentement_photo !== undefined) {
                userLive.consentement_photo = dto.consentement_photo;
                if (dto.consentement_photo) {
                    userLive.date_consentement_photo = new Date();
                }
            }
            if (dto.date_naissance !== undefined) {
                userLive.date_naissance = new Date(dto.date_naissance);
            }
            if (dto.lieu_naissance_ville !== undefined) {
                userLive.lieu_naissance_ville = dto.lieu_naissance_ville;
            }
            if (dto.lieu_naissance_pays !== undefined) {
                userLive.lieu_naissance_pays = dto.lieu_naissance_pays;
            }

            const am = await manager.findOne(AssistanteMaternelle, { where: { user_id: user.id } });
            if (!am) {
                throw new NotFoundException('Fiche assistante maternelle introuvable.');
            }

            if (dto.nir !== undefined) {
                const nirNormalized = dto.nir.replace(/\s/g, '').toUpperCase();
                const nirDejaUtilise = await manager.findOne(AssistanteMaternelle, {
                    where: { nir: nirNormalized },
                });
                if (nirDejaUtilise && nirDejaUtilise.user_id !== user.id) {
                    throw new ConflictException(
                        'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
                    );
                }
                am.nir = nirNormalized;
            }
            if (dto.numero_agrement !== undefined) {
                const agrement = dto.numero_agrement.trim();
                const agrementDejaUtilise = await manager.findOne(AssistanteMaternelle, {
                    where: { approval_number: agrement },
                });
                if (agrementDejaUtilise && agrementDejaUtilise.user_id !== user.id) {
                    throw new ConflictException(
                        'Un compte assistante maternelle avec ce numéro d\'agrément existe déjà.',
                    );
                }
                am.approval_number = agrement;
            }
            if (dto.date_agrement !== undefined) {
                am.agreement_date = new Date(dto.date_agrement);
            }
            if (dto.capacite_accueil !== undefined) {
                am.max_children = dto.capacite_accueil;
            }
            if (dto.places_disponibles !== undefined) {
                am.places_available = dto.places_disponibles;
            }
            if (dto.biographie !== undefined) {
                am.biography = dto.biographie;
            }

            userLive.statut = StatutUtilisateurType.EN_ATTENTE;
            userLive.token_reprise = undefined;
            userLive.token_reprise_expire_le = undefined;

            await manager.save(AssistanteMaternelle, am);
            await manager.save(Users, userLive);

            return {
                message: 'Dossier resoumis avec succès. Il est de nouveau en attente de validation.',
                statut: StatutUtilisateurType.EN_ATTENTE,
                user_id: userLive.id,
                numero_dossier: userLive.numero_dossier,
            };
        });
    }

    private async getFamilyChildrenIds(
        manager: EntityManager,
        parentUserId: string,
    ): Promise<Set<string>> {
        const familyUserIds = await this.parentsService.getFamilyUserIds(parentUserId);
        const links = await manager.find(ParentsChildren, {
            where: { parentId: In(familyUserIds) },
        });
        return new Set(links.map((l) => l.enfantId));
    }

    /** POST reprise-identify : numero_dossier + email → type + token. Ticket #111 */
    async identifyReprise(numero_dossier: string, email: string): Promise<RepriseIdentifyResponseDto> {
        const user = await this.usersService.findByNumeroDossierAndEmailForReprise(numero_dossier, email);
        if (!user || !user.token_reprise) {
            throw new NotFoundException('Aucun dossier en reprise trouvé pour ce numéro et cet email.');
        }
        return {
            type: user.role === RoleType.PARENT ? 'parent' : 'assistante_maternelle',
            token: user.token_reprise,
        };
    }

    /** Violation unique PostgreSQL (ex. email déjà présent malgré course entre requêtes). */
    private isPostgresUniqueViolation(err: unknown): boolean {
        if (!(err instanceof QueryFailedError)) {
            return false;
        }
        const code = (err.driverError as { code?: string } | undefined)?.code;
        return code === '23505';
    }
}
