import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { Parents } from 'src/entities/parents.entity';
import { DossierFamille } from 'src/entities/dossier_famille.entity';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateParentDto } from '../user/dto/create_parent.dto';
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
import { PendingFamilyDto } from './dto/pending-family.dto';
import {
  DossierFamilleCompletDto,
  DossierFamilleParentDto,
  DossierFamilleEnfantDto,
} from './dto/dossier-famille-complet.dto';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { Children } from 'src/entities/children.entity';
import { UpdateParentFicheAdminDto } from './dto/update-parent-fiche-admin.dto';

@Injectable()
export class ParentsService {
  constructor(
    @InjectRepository(Parents)
    private readonly parentsRepository: Repository<Parents>,
    @InjectRepository(Users)
    private readonly usersRepository: Repository<Users>,
    @InjectRepository(DossierFamille)
    private readonly dossierFamilleRepository: Repository<DossierFamille>,
    @InjectRepository(ParentsChildren)
    private readonly parentsChildrenRepository: Repository<ParentsChildren>,
  ) {}

  // Création d’un parent
  async create(dto: CreateParentDto): Promise<Parents> {
    const user = await this.usersRepository.findOneBy({ id: dto.user_id });
    if (!user) throw new NotFoundException('Utilisateur introuvable');
    if (user.role !== RoleType.PARENT) {
      throw new BadRequestException('Accès réservé aux parents');
    }

    const exist = await this.parentsRepository.findOneBy({ user_id: dto.user_id });
    if (exist) throw new ConflictException('Ce parent existe déjà');

    let co_parent: Users | null = null;
    if (dto.co_parent_id) {
      co_parent = await this.usersRepository.findOneBy({ id: dto.co_parent_id });
      if (!co_parent) throw new NotFoundException('Co-parent introuvable');
      if (co_parent.role !== RoleType.PARENT) {
        throw new BadRequestException('Accès réservé aux parents');
      }
    }

    const entity = this.parentsRepository.create({
      user_id: dto.user_id,
      user,
      co_parent: co_parent ?? undefined,
    });

    return this.parentsRepository.save(entity);
  }

  // Liste des parents
  async findAll(): Promise<Parents[]> {
    return this.parentsRepository.find({
      relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers'],
    });
  }

  // Récupérer un parent par user_id
  async findOne(user_id: string): Promise<Parents> {
    const parent = await this.parentsRepository.findOne({
      where: { user_id },
      relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers'],
    });
    if (!parent) throw new NotFoundException('Parent introuvable');
    return parent;
  }

  // Mise à jour
  async update(id: string, dto: UpdateParentsDto): Promise<Parents> {
    await this.parentsRepository.update(id, dto);
    return this.findOne(id);
  }

  /**
   * Mise à jour fiche parent (champs user + statut) par admin/gestionnaire. Ticket #131 / doc 28 §6.1.
   */
  async updateFicheAdmin(parentUserId: string, dto: UpdateParentFicheAdminDto): Promise<Parents> {
    const parent = await this.findOne(parentUserId);
    const user = parent.user;

    if (dto.email && dto.email !== user.email) {
      const existing = await this.usersRepository.findOne({ where: { email: dto.email } });
      if (existing && existing.id !== user.id) {
        throw new ConflictException('Cet email est déjà utilisé');
      }
      user.email = dto.email;
    }

    if (dto.nom !== undefined) user.nom = dto.nom;
    if (dto.prenom !== undefined) user.prenom = dto.prenom;
    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;
    if (dto.statut !== undefined) user.statut = dto.statut;

    await this.usersRepository.save(user);
    return this.findOne(parentUserId);
  }

  /**
   * Rattacher un enfant existant à un parent (enfants_parents). Ticket #115 / doc 28 §6.2.
   */
  async attachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
    await this.findOne(parentUserId);

    const existing = await this.parentsChildrenRepository.findOne({
      where: { parentId: parentUserId, enfantId },
    });
    if (existing) {
      throw new ConflictException('Cet enfant est déjà rattaché à ce parent');
    }

    const child = await this.parentsRepository.manager.findOne(Children, { where: { id: enfantId } });
    if (!child) {
      throw new NotFoundException('Enfant introuvable');
    }

    await this.parentsChildrenRepository.save(
      this.parentsChildrenRepository.create({ parentId: parentUserId, enfantId }),
    );
    return this.findOne(parentUserId);
  }

  /**
   * Détacher un enfant d'un parent sans supprimer l'enfant.
   * Autorise le détachement du dernier responsable (#157) — l'enfant reste listé
   * via GET /enfants avec parentLinks vides (alerte front).
   */
  async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
    await this.findOne(parentUserId);

    const link = await this.parentsChildrenRepository.findOne({
      where: { parentId: parentUserId, enfantId },
    });
    if (!link) {
      throw new NotFoundException('Lien parent-enfant introuvable');
    }

    await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId });
    return this.findOne(parentUserId);
  }

  /**
   * Liste des familles en attente (une entrée par famille).
   * Famille = lien co_parent ou partage d'enfants (même logique que backfill #103).
   * Uniquement les parents dont l'utilisateur a statut = en_attente.
   */
  async getPendingFamilies(): Promise<PendingFamilyDto[]> {
    let raw: {
      libelle: string;
      parentIds: unknown;
      numero_dossier: string | null;
      date_soumission: Date | string | null;
      nombre_enfants: string | number | null;
      emails: unknown;
      parents: unknown;
    }[];
    try {
      raw = await this.parentsRepository.query(`
        WITH RECURSIVE
        links AS (
          SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
          UNION ALL
          SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
          UNION ALL
          SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
          FROM enfants_parents ep1
          JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
          UNION ALL
          SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
          FROM enfants_parents ep1
          JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
        ),
        rec AS (
          SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
          UNION
          SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1
        ),
        family_rep AS (
          SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
        )
        SELECT
          'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
          array_agg(p.id_utilisateur ORDER BY u.nom, u.prenom, u.id) AS "parentIds",
          (array_agg(p.numero_dossier))[1] AS numero_dossier,
          MIN(u.cree_le) AS date_soumission,
          COALESCE((
            SELECT COUNT(DISTINCT ep.id_enfant)::int
            FROM enfants_parents ep
            WHERE ep.id_parent IN (
              SELECT frx.id FROM family_rep frx WHERE frx.rep = fr.rep
            )
          ), 0) AS nombre_enfants,
          array_agg(u.email ORDER BY u.nom, u.prenom, u.id) AS emails,
          json_agg(
            json_build_object(
              'id', u.id::text,
              'email', u.email,
              'telephone', u.telephone,
              'code_postal', u.code_postal,
              'ville', u.ville
            )
            ORDER BY u.nom, u.prenom, u.id
          ) AS parents
        FROM family_rep fr
        JOIN parents p ON p.id_utilisateur = fr.id
        JOIN utilisateurs u ON u.id = p.id_utilisateur
        WHERE u.role = 'parent' AND u.statut = 'en_attente'
        GROUP BY fr.rep
        ORDER BY libelle
      `);
    } catch (err) {
      throw err;
    }
    if (!Array.isArray(raw)) return [];
    return raw.map((r) => ({
      libelle: r.libelle ?? '',
      parentIds: this.normalizeParentIds(r.parentIds),
      numero_dossier: r.numero_dossier ?? null,
      date_soumission: this.toIsoDateTimeOrNull(r.date_soumission),
      nombre_enfants: this.normalizeNombreEnfants(r.nombre_enfants),
      emails: this.normalizeEmails(r.emails),
      parents: this.normalizeParents(r.parents),
    }));
  }

  private toIsoDateTimeOrNull(value: Date | string | null | undefined): string | null {
    if (value == null) return null;
    if (value instanceof Date) return value.toISOString();
    const d = new Date(value);
    return Number.isNaN(d.getTime()) ? null : d.toISOString();
  }

  private normalizeNombreEnfants(v: string | number | null | undefined): number {
    if (v == null) return 0;
    const n = typeof v === 'number' ? v : parseInt(String(v), 10);
    return Number.isFinite(n) && n >= 0 ? n : 0;
  }

  private normalizeEmails(emails: unknown): string[] {
    if (Array.isArray(emails)) return emails.map(String);
    if (typeof emails === 'string') {
      const s = emails.replace(/^\{|\}$/g, '').trim();
      return s ? s.split(',').map((x) => x.trim()) : [];
    }
    return [];
  }

  private normalizeParents(parents: unknown): { id: string; email: string; telephone: string | null; code_postal: string | null; ville: string | null }[] {
    if (Array.isArray(parents)) {
      return parents.map((p: any) => ({
        id: String(p?.id ?? ''),
        email: String(p?.email ?? ''),
        telephone: p?.telephone != null ? String(p.telephone) : null,
        code_postal: p?.code_postal != null ? String(p.code_postal) : null,
        ville: p?.ville != null ? String(p.ville) : null,
      }));
    }
    if (typeof parents === 'string') {
      try {
        const parsed = JSON.parse(parents);
        return this.normalizeParents(parsed);
      } catch {
        return [];
      }
    }
    return [];
  }

  /** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */
  private childToDossierFamilleEnfantDto(child: Children): DossierFamilleEnfantDto {
    return {
      id: child.id,
      first_name: child.first_name,
      last_name: child.last_name,
      genre: child.gender,
      birth_date: child.birth_date,
      due_date: child.due_date,
      status: child.status,
      photo_url: child.photo_url ?? undefined,
      consent_photo: child.consent_photo,
      est_multiple: child.is_multiple,
    };
  }

  private normalizeParentIds(parentIds: unknown): string[] {
    if (Array.isArray(parentIds)) return parentIds.map(String);
    if (typeof parentIds === 'string') {
      const s = parentIds.replace(/^\{|\}$/g, '').trim();
      return s ? s.split(',').map((x) => x.trim()) : [];
    }
    return [];
  }

  /**
   * Dossier famille complet par numéro de dossier. Ticket #119.
   * Rôles : admin, gestionnaire.
   * @throws NotFoundException si aucun parent avec ce numéro de dossier
   */
  async getDossierFamilleByNumero(numeroDossier: string): Promise<DossierFamilleCompletDto> {
    const num = numeroDossier?.trim();
    if (!num) {
      throw new NotFoundException('Numéro de dossier requis.');
    }
    const firstParent = await this.parentsRepository.findOne({
      where: { numero_dossier: num },
      relations: ['user'],
    });
    if (!firstParent || !firstParent.user) {
      throw new NotFoundException('Aucun dossier famille trouvé pour ce numéro.');
    }
    const familyUserIds = await this.getFamilyUserIds(firstParent.user_id);
    const parents = await this.parentsRepository.find({
      where: { user_id: In(familyUserIds) },
      relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers', 'dossiers.child'],
    });
    const enfantsMap = new Map<string, DossierFamilleEnfantDto>();
    let texte_motivation: string | undefined;

    // Un dossier = une famille, un seul texte de motivation
    const dossierFamille = await this.dossierFamilleRepository.findOne({
      where: { numero_dossier: num },
      relations: ['parent', 'enfants', 'enfants.enfant'],
    });
    if (dossierFamille?.presentation) {
      texte_motivation = dossierFamille.presentation;
    }

    for (const p of parents) {
      // Enfants via parentChildren
      if (p.parentChildren) {
        for (const pc of p.parentChildren) {
          if (pc.child && !enfantsMap.has(pc.child.id)) {
            enfantsMap.set(pc.child.id, this.childToDossierFamilleEnfantDto(pc.child));
          }
        }
      }
      // Fallback : anciens dossiers (un texte, on prend le premier)
      if (texte_motivation == null && p.dossiers?.length) {
        texte_motivation = p.dossiers[0].presentation ?? undefined;
      }
    }

    // Enfants uniquement liés via dossier_famille_enfants (legacy / parcours alternatif)
    if (dossierFamille?.enfants?.length) {
      for (const dfe of dossierFamille.enfants) {
        const c = dfe.enfant;
        if (c && !enfantsMap.has(c.id)) {
          enfantsMap.set(c.id, this.childToDossierFamilleEnfantDto(c));
        }
      }
    }

    const parentsDto: DossierFamilleParentDto[] = parents.map((p) => ({
      user_id: p.user_id,
      email: p.user.email,
      prenom: p.user.prenom,
      nom: p.user.nom,
      telephone: p.user.telephone,
      adresse: p.user.adresse,
      ville: p.user.ville,
      code_postal: p.user.code_postal,
      statut: p.user.statut,
      co_parent_id: p.co_parent?.id,
    }));
    return {
      numero_dossier: num,
      parents: parentsDto,
      enfants: Array.from(enfantsMap.values()),
      texte_motivation,
    };
  }

  /**
   * Retourne les user_id de tous les parents de la même famille (co_parent ou enfants partagés).
   * @throws NotFoundException si parentId n'est pas un parent
   */
  async getFamilyUserIds(parentId: string): Promise<string[]> {
    const raw = await this.parentsRepository.query(
      `
      WITH RECURSIVE
      links AS (
        SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
        UNION ALL
        SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
        UNION ALL
        SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
        FROM enfants_parents ep1
        JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
        UNION ALL
        SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
        FROM enfants_parents ep1
        JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
      ),
      rec AS (
        SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
        UNION
        SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1
      ),
      family_rep AS (
        SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
      ),
      input_rep AS (
        SELECT rep FROM family_rep WHERE id = $1::uuid LIMIT 1
      )
      SELECT fr.id::text AS id
      FROM family_rep fr
      CROSS JOIN input_rep ir
      WHERE fr.rep = ir.rep
      `,
      [parentId],
    );
    if (!raw || raw.length === 0) {
      throw new NotFoundException('Parent introuvable ou pas encore enregistré en base.');
    }
    return raw.map((r: { id: string }) => r.id);
  }
}
