"""
Modeles CMS pour la gestion des pages, sections, menus et parametres.
"""
import os
from decimal import Decimal
from django.db import models
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.conf import settings
from django.utils.html import format_html

from config.models import Orderable, Timestamped, Activatable
from image_factory.models import Imageable
from traduction.helpers import TranslatableMixin


# =============================================================================
# Template
# =============================================================================

class Template(Timestamped, Activatable):
    """
    Modele de template pour les pages.

    Structure de repertoire creee automatiquement:

    cms/templates/{template_dir}/
        base.html        # Template de base specifique
        index.html       # Fichier principal heritant de base.html
        sections/        # Dossier pour les sections
        partials/        # Dossier pour les composants reutilisables

    cms/static/{template_dir}/
        css/             # Fichiers CSS specifiques
        js/              # Fichiers JS specifiques
    """
    name = models.CharField(_("Nom"), max_length=100)
    slug = models.SlugField(_("Slug"), max_length=100, unique=True, blank=True)
    template_dir = models.CharField(
        _("Repertoire"),
        max_length=100,
        help_text=_("Nom du repertoire (cree dans cms/templates/ et cms/static/)")
    )
    base_file = models.CharField(
        _("Fichier de base"),
        max_length=100,
        default='index.html',
        help_text=_("Fichier principal du template (ex: index.html)")
    )
    description = models.TextField(_("Description"), blank=True)

    class Meta:
        verbose_name = _("Template")
        verbose_name_plural = _("Templates")
        ordering = ['name']

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)

        # Verifier si c'est une creation (pas de pk encore)
        is_new = self.pk is None

        super().save(*args, **kwargs)

        # Creer la structure de dossiers et fichiers pour un nouveau template
        if is_new:
            self._create_template_structure()
            self._create_static_structure()

    def _get_cms_app_path(self):
        """Retourne le chemin de l'application CMS."""
        return os.path.dirname(os.path.abspath(__file__))

    def _create_template_structure(self):
        """Cree la structure de dossiers et fichiers du template."""
        base_path = self.get_base_path()

        # Creer les dossiers
        os.makedirs(base_path, exist_ok=True)
        os.makedirs(self.get_sections_path(), exist_ok=True)
        os.makedirs(self.get_partials_path(), exist_ok=True)
        
        # Creation de section-title.html
        self.create_section_title_file()

        # Creer base.html s'il n'existe pas
        base_html_path = os.path.join(base_path, 'base.html')
        if not os.path.exists(base_html_path):
            with open(base_html_path, 'w', encoding='utf-8') as f:
                f.write(self._get_base_html_content())

        # Creer index.html (ou base_file) s'il n'existe pas
        index_path = os.path.join(base_path, self.base_file)
        if not os.path.exists(index_path):
            with open(index_path, 'w', encoding='utf-8') as f:
                f.write(self._get_index_html_content())

    def _create_static_structure(self):
        """Cree la structure de dossiers static du template."""
        static_path = self.get_static_path()

        # Creer les dossiers static
        os.makedirs(static_path, exist_ok=True)
        os.makedirs(os.path.join(static_path, 'css'), exist_ok=True)
        os.makedirs(os.path.join(static_path, 'js'), exist_ok=True)

        # Creer un fichier CSS vide
        css_file = os.path.join(static_path, 'css', 'style.css')
        if not os.path.exists(css_file):
            with open(css_file, 'w', encoding='utf-8') as f:
                f.write(f'''/* =============================================================================
   Styles pour le template: {self.name}
   ============================================================================= */

''')

        # Creer un fichier JS vide
        js_file = os.path.join(static_path, 'js', 'main.js')
        if not os.path.exists(js_file):
            with open(js_file, 'w', encoding='utf-8') as f:
                f.write(f'''/* =============================================================================
   Scripts pour le template: {self.name}
   ============================================================================= */

document.addEventListener('DOMContentLoaded', function() {{
    // Initialisation
}});
''')

    def _get_base_html_content(self):
        """Retourne le contenu du fichier base.html."""
        return '''{% extends 'base.html' %}
{% load static cms_tags %}

{# ============================================================================= #}
{# Template de base pour: ''' + self.name + ''' #}
{# ============================================================================= #}

{% block title %}{% block page_title %}{% endblock %} - {{ site_config.site_name }}{% endblock %}

{% block meta_description %}{% block page_description %}{% endblock %}{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'cms/''' + self.template_dir + '''/css/style.css' %}">
{% block page_css %}{% endblock %}
{% endblock %}

{% block content %}
{% block page_content %}
{% endblock %}
{% endblock %}

{% block extra_js %}
<script src="{% static 'cms/''' + self.template_dir + '''/js/main.js' %}"></script>
{% block page_js %}{% endblock %}
{% endblock %}
'''

    def _get_index_html_content(self):
        """Retourne le contenu du fichier index.html."""
        return '''{% extends "cms/''' + self.template_dir + '''/base.html" %}
{% load static cms_tags %}

{# ============================================================================= #}
{# Page principale: ''' + self.name + ''' #}
{# ============================================================================= #}

{% block page_title %}{{ page.title|default:"''' + self.name + '''" }}{% endblock %}

{% block page_description %}{{ page.description|default:"" }}{% endblock %}

{% block page_content %}
{# Affiche les sections de la page dans l'ordre defini #}
{% for section in page.get_ordered_sections %}
    {% include_section section %}
{% empty %}
    <div class="py-16 text-center">
        <p class="text-gray-500">Aucune section configuree pour cette page.</p>
    </div>
{% endfor %}
{% endblock %}
'''

    def get_base_path(self):
        """Retourne le chemin absolu du repertoire templates du template."""
        return os.path.join(self._get_cms_app_path(), 'templates', 'cms', self.template_dir)

    def get_static_path(self):
        """Retourne le chemin absolu du repertoire static du template."""
        return os.path.join(self._get_cms_app_path(), 'static', 'cms', self.template_dir)

    def get_sections_path(self):
        """Retourne le chemin du repertoire sections."""
        return os.path.join(self.get_base_path(), 'sections')

    def get_partials_path(self):
        """Retourne le chemin du repertoire partials."""
        return os.path.join(self.get_base_path(), 'partials')

    def get_template_name(self):
        """Retourne le chemin du template pour Django."""
        return f"cms/{self.template_dir}/{self.base_file}"

    def section_file_exists(self, filename):
        """Verifie si un fichier de section existe."""
        file_path = os.path.join(self.get_sections_path(), f"{filename}.html")
        return os.path.exists(file_path)

    def create_section_file(self, filename, title=''):
        """
        Cree un fichier de section vide.

        Args:
            filename: Nom du fichier sans extension
            title: Titre de la section (optionnel)
        """
        section_path = os.path.join(self.get_sections_path(), f"{filename}.html")
        if not os.path.exists(section_path):
            content = '''{% load static %}
{# Section: ''' + (title or filename) + ''' #}

<section class="py-16">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        {% include 'cms/''' + (self.template_dir) + '''/partials/section-title.html' %}
        
        {# Contenu de la section #}
        <div>
            <!-- TODO: Ajouter le contenu de la section -->
        </div>
    </div>
</section>
'''
            with open(section_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return True
        return False


    def create_section_title_file(self):
        """
        Cree le fichier de section-title .

        Args:
            filename: Nom du fichier sans extension
            title: Titre de la section (optionnel)
        """
        section_title_path = os.path.join(self.get_partials_path(), f"section-title.html")
        if not os.path.exists(section_title_path):
            content = '''

        {% if section.title %}
        <div class="text-center mb-12">
            <h2 class="text-3xl font-bold text-primary mb-4">{{ section.title }}</h2>
            {% if section.description %}
            <p class="text-gray-600 max-w-2xl mx-auto">{{ section.description }}</p>
            {% endif %}
        </div>
        {% endif %}
'''
            with open(section_title_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return True
        return False


# =============================================================================
# TemplateFile
# =============================================================================

class TemplateFile(Timestamped):
    """
    Fichier supplementaire pour un template (autre.html, contact.html, etc.).
    Herite automatiquement de base.html du template parent.
    """
    template = models.ForeignKey(
        Template,
        on_delete=models.CASCADE,
        related_name='files',
        verbose_name=_("Template")
    )
    name = models.CharField(
        _("Nom"),
        max_length=100,
        help_text=_("Nom descriptif du fichier")
    )
    file = models.CharField(
        _("Fichier"),
        max_length=100,
        help_text=_("Nom du fichier sans extension .html")
    )
    description = models.TextField(_("Description"), blank=True)

    class Meta:
        verbose_name = _("Fichier de template")
        verbose_name_plural = _("Fichiers de template")
        ordering = ['template', 'name']
        constraints = [
            models.UniqueConstraint(
                fields=['template', 'file'],
                name='unique_template_file'
            )
        ]

    def __str__(self):
        return f"{self.file}.html ({self.template.name})"

    def save(self, *args, **kwargs):
        is_new = self.pk is None
        super().save(*args, **kwargs)

        # Creer le fichier s'il n'existe pas
        if is_new:
            self._create_file()

    def _create_file(self):
        """Cree le fichier HTML."""
        file_path = self.get_file_path()
        if not os.path.exists(file_path):
            content = self._get_file_content()
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)

    def _get_file_content(self):
        """Retourne le contenu du fichier."""
        return '''{% extends "cms/''' + self.template.template_dir + '''/base.html" %}
{% load static cms_tags %}

{# ============================================================================= #}
{# ''' + self.name + ''' #}
{# ============================================================================= #}

{% block page_title %}{{ page.title|default:"''' + self.name + '''" }}{% endblock %}

{% block page_description %}{{ page.description|default:"" }}{% endblock %}

{% block page_content %}
{# Affiche les sections de la page dans l'ordre defini #}
{% for section in page.get_ordered_sections %}
    {% include_section section %}
{% empty %}
    <div class="py-16 text-center">
        <p class="text-gray-500">Aucune section configuree pour cette page.</p>
    </div>
{% endfor %}
{% endblock %}
'''

    def get_file_path(self):
        """Retourne le chemin absolu du fichier."""
        return os.path.join(self.template.get_base_path(), f"{self.file}.html")

    def get_template_name(self):
        """Retourne le chemin du template pour Django."""
        return f"cms/{self.template.template_dir}/{self.file}.html"

    def file_exists(self):
        """Verifie si le fichier existe."""
        return os.path.exists(self.get_file_path())


# =============================================================================
# Section
# =============================================================================

class Section(TranslatableMixin, Imageable, Timestamped):
    """
    Section reutilisable pouvant etre ajoutee a plusieurs pages.
    Chaque section correspond a un fichier dans le repertoire sections/ du template.
    Le fichier est cree automatiquement s'il n'existe pas.

    Note: L'ordre des sections est gere par PageSection, pas ici.
    """
    name = models.CharField(_("Nom"), max_length=100)
    file = models.CharField(
        _("Fichier"),
        max_length=100,
        help_text=_("Nom du fichier sans extension .html (cree automatiquement)")
    )
    title = models.CharField(_("Titre"), max_length=255, blank=True)
    description = models.TextField(_("Description"), blank=True)
    template = models.ForeignKey(
        Template,
        on_delete=models.CASCADE,
        related_name='sections',
        verbose_name=_("Template")
    )

    class Meta:
        verbose_name = _("Section")
        verbose_name_plural = _("Sections")
        ordering = ['name']

    def __str__(self):
        title = self.get_translated('title') or self.title or self.name
        return f"{title} ({self.template.name})"

    def save(self, *args, **kwargs):
        # Verifier si c'est une creation
        is_new = self.pk is None

        super().save(*args, **kwargs)

        # Creer le fichier de section s'il n'existe pas
        if is_new and self.template_id and self.file:
            self.template.create_section_file(self.file, self.title or self.name)

    def get_template_path(self):
        """Retourne le chemin complet du template de section."""
        return f"cms/{self.template.template_dir}/sections/{self.file}.html"

    def file_exists(self):
        """Verifie si le fichier de section existe."""
        return self.template.section_file_exists(self.file)


# =============================================================================
# Page
# =============================================================================

class Page(TranslatableMixin, Timestamped):
    """
    Page CMS composee de sections ordonnees.
    """
    name = models.CharField(_("Nom"), max_length=100)
    slug = models.SlugField(_("Slug"), max_length=100, unique=True, blank=True)
    title = models.CharField(_("Titre"), max_length=255)
    description = models.TextField(_("Description"), blank=True)
    keywords_text = models.CharField(
        _("Mots-cles"),
        max_length=500,
        blank=True,
        help_text=_("Separes par des virgules (ex: django, python, web). Cree automatiquement les mots-cles.")
    )
    template = models.ForeignKey(
        Template,
        on_delete=models.PROTECT,
        related_name='pages',
        verbose_name=_("Template")
    )
    template_file = models.ForeignKey(
        TemplateFile,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='pages',
        verbose_name=_("Fichier de template"),
        help_text=_("Fichier specifique a utiliser (par defaut: index.html)")
    )
    sections = models.ManyToManyField(
        Section,
        through='PageSection',
        related_name='pages',
        verbose_name=_("Sections"),
        blank=True
    )
    is_active = models.BooleanField(_("Active"), default=True)
    is_homepage = models.BooleanField(_("Page d'accueil"), default=False)

    class Meta:
        verbose_name = _("Page")
        verbose_name_plural = _("Pages")
        ordering = ['name']

    def __str__(self):
        return self.get_translated('title') or self.title or self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        # Une seule page d'accueil
        if self.is_homepage:
            Page.objects.filter(is_homepage=True).exclude(pk=self.pk).update(is_homepage=False)
        super().save(*args, **kwargs)

        # Synchroniser les mots-cles depuis keywords_text
        self._sync_keywords()

    def _sync_keywords(self):
        """Synchronise les KeywordAssignments depuis keywords_text."""
        if self.pk:
            keywords_list = self.keywords  # Utilise la property
            KeywordAssignment.set_keywords_for_object(self, keywords_list)

    def get_template_name(self):
        """Retourne le chemin du template a utiliser."""
        if self.template_file:
            return self.template_file.get_template_name()
        return self.template.get_template_name()

    @property
    def keywords(self):
        """Retourne les mots-cles sous forme de liste."""
        if self.keywords_text:
            return [kw.strip() for kw in self.keywords_text.split(',') if kw.strip()]
        return []

    def get_ordered_sections(self):
        """Retourne les sections ordonnees par PageSection.order."""
        return self.sections.filter(
            pagesection__page=self
        ).order_by('pagesection__order')

    def get_ordered_page_sections(self):
        """Retourne les PageSection ordonnees avec leurs sections pre-chargees."""
        return PageSection.objects.filter(
            page=self
        ).select_related('section', 'section__template').order_by('order')

    def get_absolute_url(self):
        if self.is_homepage:
            return reverse('cms:home')
        return reverse('cms:page', kwargs={'slug': self.slug})


# =============================================================================
# PageSection (Through Model)
# =============================================================================

class PageSection(Orderable):
    """
    Modele intermediaire pour la relation Page-Section avec ordre.
    L'ordre est automatiquement defini a max + 1 lors de la creation.
    """
    page = models.ForeignKey(
        Page,
        on_delete=models.CASCADE,
        verbose_name=_("Page")
    )
    section = models.ForeignKey(
        Section,
        on_delete=models.CASCADE,
        verbose_name=_("Section")
    )

    class Meta:
        verbose_name = _("Section de page")
        verbose_name_plural = _("Sections de page")
        ordering = ['order']
        constraints = [
            models.UniqueConstraint(
                fields=['page', 'section'],
                name='unique_page_section'
            )
        ]

    def __str__(self):
        return f"{self.page.name} - {self.section.name} (#{self.order})"

    def save(self, *args, **kwargs):
        # Si c'est une creation et l'ordre n'est pas defini
        if self.pk is None and (self.order is None or self.order == 0):
            # Calculer l'ordre max pour cette page
            max_order = PageSection.objects.filter(page=self.page).aggregate(
                max_order=models.Max('order')
            )['max_order']
            self.order = (max_order or 0) + 1
        super().save(*args, **kwargs)

    def get_field_value(self, field_slug, default=None, language_code=None):
        """
        Recupere la valeur d'un champ personnalise.

        Args:
            field_slug: Slug du SectionField
            default: Valeur par defaut si non trouvee
            language_code: Code de la langue pour la traduction (optionnel)

        Returns:
            La valeur du champ ou default
        """
        try:
            field_value = self.field_values.select_related('field').get(
                field__slug=field_slug
            )
            value = field_value.get_value(language_code=language_code)
            return value if value is not None else default
        except PageSectionFieldValue.DoesNotExist:
            # Essayer de recuperer la valeur par defaut du champ
            try:
                field = self.section.custom_fields.get(slug=field_slug)
                return field.default_value or default
            except SectionField.DoesNotExist:
                return default

    def get_all_field_values(self, language_code=None):
        """
        Recupere toutes les valeurs de champs sous forme de dictionnaire.

        Args:
            language_code: Code de la langue pour la traduction (optionnel)

        Returns:
            Dict {field_slug: value}
        """
        result = {}

        # Recuperer toutes les definitions de champs pour cette section
        fields = {f.slug: f for f in self.section.custom_fields.all()}

        # Recuperer les valeurs stockees
        stored_values = {
            fv.field.slug: fv.get_value(language_code=language_code)
            for fv in self.field_values.select_related('field').all()
        }

        # Fusionner avec les valeurs par defaut
        for slug, field in fields.items():
            if slug in stored_values and stored_values[slug] is not None:
                result[slug] = stored_values[slug]
            else:
                result[slug] = field.default_value or None

        return result

    def set_field_value(self, field_slug, value):
        """
        Definit la valeur d'un champ personnalise.

        Args:
            field_slug: Slug du SectionField
            value: Nouvelle valeur
        """
        field = self.section.custom_fields.get(slug=field_slug)
        field_value, created = PageSectionFieldValue.objects.get_or_create(
            page_section=self,
            field=field
        )
        field_value.set_value(value)
        field_value.save()


# =============================================================================
# SectionField (Champs personnalises pour Section)
# =============================================================================

class SectionField(Orderable, Timestamped):
    """
    Definition d'un champ personnalise pour une Section.
    Les valeurs sont stockees dans PageSectionFieldValue.
    """
    FIELD_TYPE_CHOICES = [
        ('text', _('Texte court')),
        ('textarea', _('Texte long')),
        ('boolean', _('Case a cocher')),
        ('integer', _('Nombre entier')),
        ('decimal', _('Nombre decimal')),
        ('url', _('URL')),
        ('email', _('Email')),
        ('image', _('Image')),
        ('select', _('Liste de choix')),
    ]

    section = models.ForeignKey(
        Section,
        on_delete=models.CASCADE,
        related_name='custom_fields',
        verbose_name=_("Section")
    )
    name = models.CharField(
        _("Nom technique"),
        max_length=100,
        help_text=_("Nom utilise dans le code (ex: button_text)")
    )
    slug = models.SlugField(
        _("Slug"),
        max_length=100,
        blank=True,
        help_text=_("Auto-genere depuis le nom")
    )
    label = models.CharField(
        _("Libelle"),
        max_length=255,
        help_text=_("Libelle affiche dans l'admin")
    )
    field_type = models.CharField(
        _("Type de champ"),
        max_length=20,
        choices=FIELD_TYPE_CHOICES,
        default='text'
    )
    required = models.BooleanField(
        _("Obligatoire"),
        default=False
    )
    default_value = models.TextField(
        _("Valeur par defaut"),
        blank=True,
        help_text=_("Valeur utilisee si non renseignee")
    )
    help_text = models.CharField(
        _("Texte d'aide"),
        max_length=500,
        blank=True
    )
    choices = models.TextField(
        _("Choix (pour type select)"),
        blank=True,
        help_text=_("Un choix par ligne, format: valeur|libelle ou juste valeur")
    )

    class Meta:
        verbose_name = _("Champ personnalise")
        verbose_name_plural = _("Champs personnalises")
        ordering = ['order']
        constraints = [
            models.UniqueConstraint(
                fields=['section', 'slug'],
                name='unique_section_field_slug'
            )
        ]

    def __str__(self):
        return f"{self.label} ({self.section.name})"

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def get_choices_list(self):
        """Retourne les choix sous forme de liste de tuples."""
        if not self.choices:
            return []
        choices_list = []
        for line in self.choices.strip().split('\n'):
            line = line.strip()
            if '|' in line:
                value, label = line.split('|', 1)
                choices_list.append((value.strip(), label.strip()))
            else:
                choices_list.append((line, line))
        return choices_list


# =============================================================================
# PageSectionFieldValue (Valeurs des champs par PageSection)
# =============================================================================

class PageSectionFieldValue(TranslatableMixin, Timestamped):
    """
    Valeur d'un champ personnalise pour une instance PageSection.
    Permet des valeurs differentes pour la meme section sur differentes pages.
    Le champ text_value est traduisible.
    """
    page_section = models.ForeignKey(
        PageSection,
        on_delete=models.CASCADE,
        related_name='field_values',
        verbose_name=_("Section de page")
    )
    field = models.ForeignKey(
        SectionField,
        on_delete=models.CASCADE,
        related_name='values',
        verbose_name=_("Champ")
    )

    # Colonnes de stockage par type
    text_value = models.TextField(_("Valeur texte"), blank=True, null=True)
    integer_value = models.IntegerField(_("Valeur entiere"), blank=True, null=True)
    decimal_value = models.DecimalField(
        _("Valeur decimale"),
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True
    )
    boolean_value = models.BooleanField(_("Valeur booleenne"), blank=True, null=True)
    image_value = models.ImageField(
        _("Image"),
        upload_to='cms/section_fields/',
        blank=True,
        null=True
    )

    class Meta:
        verbose_name = _("Valeur de champ")
        verbose_name_plural = _("Valeurs de champs")
        constraints = [
            models.UniqueConstraint(
                fields=['page_section', 'field'],
                name='unique_pagesection_field_value'
            )
        ]

    def __str__(self):
        return f"{self.field.label}: {self.get_value()}"

    def get_value(self, language_code=None):
        """
        Retourne la valeur selon le type de champ.
        Pour les champs texte, retourne la traduction si disponible.

        Args:
            language_code: Code de la langue (optionnel, utilise la langue courante)
        """
        field_type = self.field.field_type

        if field_type in ('text', 'textarea', 'url', 'email', 'select'):
            # Utiliser la traduction si disponible
            if self.pk and language_code:
                translated = self.get_translated('text_value', language_code, fallback=True)
                if translated:
                    return translated
            return self.text_value or self.field.default_value or ''
        elif field_type == 'integer':
            if self.integer_value is not None:
                return self.integer_value
            if self.field.default_value:
                try:
                    return int(self.field.default_value)
                except ValueError:
                    pass
            return None
        elif field_type == 'decimal':
            if self.decimal_value is not None:
                return self.decimal_value
            if self.field.default_value:
                try:
                    return Decimal(self.field.default_value)
                except Exception:
                    pass
            return None
        elif field_type == 'boolean':
            if self.boolean_value is not None:
                return self.boolean_value
            if self.field.default_value:
                return self.field.default_value.lower() in ('true', '1', 'yes', 'oui')
            return False
        elif field_type == 'image':
            return self.image_value or None

        return None

    def set_value(self, value):
        """Definit la valeur selon le type de champ."""
        field_type = self.field.field_type

        # Reset toutes les valeurs
        self.text_value = None
        self.integer_value = None
        self.decimal_value = None
        self.boolean_value = None
        # Note: image_value gere separement

        if value is None or value == '':
            return

        if field_type in ('text', 'textarea', 'url', 'email', 'select'):
            self.text_value = str(value)
        elif field_type == 'integer':
            self.integer_value = int(value) if value else None
        elif field_type == 'decimal':
            self.decimal_value = Decimal(str(value)) if value else None
        elif field_type == 'boolean':
            if isinstance(value, bool):
                self.boolean_value = value
            else:
                self.boolean_value = str(value).lower() in ('true', '1', 'yes', 'oui', 'on')


# =============================================================================
# Menu
# =============================================================================

class Menu(Timestamped, Activatable):
    """
    Menu de navigation (header, footer, sidebar, etc.).
    """
    LOCATION_CHOICES = [
        ('header', _('En-tete')),
        ('footer', _('Pied de page')),
        ('sidebar', _('Barre laterale')),
        ('topbar', _('Barre superieure')),
    ]

    name = models.CharField(_("Nom"), max_length=100)
    slug = models.SlugField(_("Slug"), max_length=100, unique=True, blank=True)
    location = models.CharField(
        _("Emplacement"),
        max_length=20,
        choices=LOCATION_CHOICES,
        default='header'
    )

    class Meta:
        verbose_name = _("Menu")
        verbose_name_plural = _("Menus")
        ordering = ['name']

    def __str__(self):
        return f"{self.name} ({self.get_location_display()})"

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def get_root_items(self):
        """
        Retourne les elements de premier niveau (sans parent).
        Utilise les items traduits si disponibles.
        """
        # Si les items traduits sont disponibles (via get_menu du context processor)
        if hasattr(self, '_translated_root_items'):
            return self._translated_root_items
        # Sinon, requete normale
        return self.items.filter(parent__isnull=True, active=True).order_by('order')


# =============================================================================
# MenuItem
# =============================================================================

class MenuItem(TranslatableMixin, Orderable, Activatable):
    """
    Element de menu avec support pour menus imbriques et URLs internes/externes.
    """
    URL_TYPE_CHOICES = [
        ('internal', _('Interne (URL nommee)')),
        ('external', _('Externe (URL complete)')),
        ('page', _('Page CMS')),
    ]

    menu = models.ForeignKey(
        Menu,
        on_delete=models.CASCADE,
        related_name='items',
        verbose_name=_("Menu")
    )
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='children',
        verbose_name=_("Parent")
    )
    title = models.CharField(_("Titre"), max_length=100)
    url_type = models.CharField(
        _("Type d'URL"),
        max_length=20,
        choices=URL_TYPE_CHOICES,
        default='internal'
    )
    internal_url = models.CharField(
        _("URL nommee"),
        max_length=100,
        blank=True,
        help_text=_("Ex: 'news:list', 'contact:contact'")
    )
    external_url = models.URLField(
        _("URL externe"),
        blank=True,
        help_text=_("URL complete avec https://")
    )
    page = models.ForeignKey(
        Page,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='menu_items',
        verbose_name=_("Page CMS")
    )
    icon = models.ForeignKey(
        'config.Icon',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='menu_items',
        verbose_name=_("Icone")
    )
    open_in_new_tab = models.BooleanField(_("Ouvrir dans un nouvel onglet"), default=False)

    class Meta:
        verbose_name = _("Element de menu")
        verbose_name_plural = _("Elements de menu")
        ordering = ['menu', 'parent__order', 'parent__id', 'order']  # Ordre par fratrie

    def __str__(self):
        prefix = "  -- " if self.parent else ""
        title = self.get_translated('title') or self.title
        return f"{prefix}{title}"

    def get_url(self):
        """Resout et retourne l'URL reelle."""
        if self.url_type == 'external':
            return self.external_url or '#'
        elif self.url_type == 'page' and self.page:
            return self.page.get_absolute_url()
        elif self.url_type == 'internal' and self.internal_url:
            try:
                return reverse(self.internal_url)
            except NoReverseMatch:
                return '#'
        return '#'

    def save(self, *args, **kwargs):
        # Si c'est une creation et l'ordre n'est pas defini
        if self.pk is None and (self.order is None or self.order == 0):
            # Calculer l'ordre max pour ce menu (et ce parent si applicable)
            if self.parent:
                max_order = MenuItem.objects.filter(
                    menu=self.menu, parent=self.parent
                ).aggregate(max_order=models.Max('order'))['max_order']
            else:
                max_order = MenuItem.objects.filter(
                    menu=self.menu, parent__isnull=True
                ).aggregate(max_order=models.Max('order'))['max_order']
            self.order = (max_order or 0) + 1
        super().save(*args, **kwargs)

    def has_children(self):
        """Verifie si l'element a des enfants actifs."""
        # Si les enfants traduits sont disponibles
        if hasattr(self, '_translated_children'):
            return len(self._translated_children) > 0
        return self.children.filter(active=True).exists()

    @property
    def has_children_prop(self):
        """Propriete pour verifier les enfants (pour les templates)."""
        return self.has_children()

    def get_children(self):
        """
        Retourne les enfants actifs ordonnes.
        Utilise les enfants traduits si disponibles.
        """
        # Si les enfants traduits sont disponibles (via get_menu du context processor)
        if hasattr(self, '_translated_children'):
            return self._translated_children
        return self.children.filter(active=True).order_by('order')


# =============================================================================
# SettingType
# =============================================================================

class SettingType(TranslatableMixin, models.Model):
    """
    Types de parametres pour le formatage et la validation des valeurs.

    Types disponibles:
        - text: Texte court (une ligne)
        - textarea: Texte long (multiligne)
        - number: Nombre entier
        - decimal: Nombre decimal
        - phone: Numero de telephone
        - email: Adresse email
        - url: URL
        - currency: Devise/montant
        - percentage: Pourcentage
        - color: Code couleur hexadecimal
        - boolean: Oui/Non
        - image: Fichier image

    Exemples de patterns:
        - Devise: format_pattern="{value} EUR"
        - Telephone: format_pattern="<a href='tel:{value}'>{value}</a>"
        - Email: format_pattern="<a href='mailto:{value}'>{value}</a>"
        - Pourcentage: format_pattern="{value}%"
    """
    VALUE_TYPE_CHOICES = [
        ('text', _('Texte')),
        ('textarea', _('Texte long')),
        ('number', _('Nombre entier')),
        ('decimal', _('Nombre decimal')),
        ('phone', _('Telephone')),
        ('email', _('Email')),
        ('url', _('URL')),
        ('currency', _('Devise')),
        ('percentage', _('Pourcentage')),
        ('color', _('Couleur')),
        ('boolean', _('Booleen (Oui/Non)')),
        ('image', _('Image')),
    ]

    # Patterns par defaut pour chaque type
    DEFAULT_PATTERNS = {
        'phone': '<a href="tel:{value}" class="setting-phone">{value}</a>',
        'email': '<a href="mailto:{value}" class="setting-email">{value}</a>',
        'url': '<a href="{value}" target="_blank" rel="noopener" class="setting-url">{value}</a>',
        'currency': '<span class="setting-currency">{value} EUR</span>',
        'percentage': '<span class="setting-percentage">{value}%</span>',
        'color': '<span class="setting-color" style="background-color:{value};width:20px;height:20px;display:inline-block;border-radius:3px;"></span>',
        'image': '<img src="{value}" alt="" class="setting-image" />',
    }

    name = models.CharField(_("Nom"), max_length=50)
    slug = models.SlugField(_("Slug"), max_length=50, unique=True, blank=True)
    value_type = models.CharField(
        _("Type de valeur"),
        max_length=15,
        choices=VALUE_TYPE_CHOICES,
        default='text',
        help_text=_("Type de donnee attendu pour ce parametre")
    )
    format_pattern = models.CharField(
        _("Pattern de formatage"),
        max_length=500,
        blank=True,
        help_text=_("Pattern HTML avec {value} comme placeholder. Laissez vide pour le pattern par defaut.")
    )
    icon = models.CharField(
        _("Icone"),
        max_length=50,
        blank=True,
        help_text=_("Classe d'icone (ex: fa-phone, fa-envelope)")
    )
    description = models.TextField(_("Description"), blank=True)

    class Meta:
        verbose_name = _("Type de parametre")
        verbose_name_plural = _("Types de parametres")
        ordering = ['name']

    def __str__(self):
        name = self.get_translated('name') or self.name
        return f"{name} ({self.get_value_type_display()})"

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    @property
    def is_image(self):
        """Retourne True si ce type attend une image."""
        return self.value_type == 'image'

    @property
    def is_text_based(self):
        """Retourne True si ce type stocke une valeur textuelle."""
        return self.value_type not in ('image', 'boolean')

    @property
    def is_numeric(self):
        """Retourne True si ce type est numerique."""
        return self.value_type in ('number', 'decimal', 'currency', 'percentage')

    def get_pattern(self):
        """Retourne le pattern de formatage (personnalise ou par defaut)."""
        if self.format_pattern:
            return self.format_pattern
        return self.DEFAULT_PATTERNS.get(self.value_type, '{value}')

    def format_value(self, value):
        """Formate une valeur selon le pattern defini."""
        if value is None or value == '':
            return ''
        pattern = self.get_pattern()
        return pattern.format(value=value)

    def validate_value(self, value):
        """
        Valide une valeur selon le type.
        Retourne (is_valid, error_message).
        """
        import re

        if value is None or value == '':
            return True, None

        value_str = str(value)

        if self.value_type == 'email':
            email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
            if not re.match(email_regex, value_str):
                return False, _("Format email invalide")

        elif self.value_type == 'url':
            url_regex = r'^https?://[^\s/$.?#].[^\s]*$'
            if not re.match(url_regex, value_str, re.IGNORECASE):
                return False, _("Format URL invalide (doit commencer par http:// ou https://)")

        elif self.value_type == 'phone':
            # Accepte formats internationaux et locaux
            phone_regex = r'^[\+]?[(]?[0-9]{1,4}[)]?[-\s\./0-9]*$'
            if not re.match(phone_regex, value_str):
                return False, _("Format telephone invalide")

        elif self.value_type == 'color':
            color_regex = r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'
            if not re.match(color_regex, value_str):
                return False, _("Format couleur invalide (ex: #FF5733)")

        elif self.value_type == 'number':
            try:
                int(value_str)
            except ValueError:
                return False, _("Nombre entier attendu")

        elif self.value_type in ('decimal', 'currency', 'percentage'):
            try:
                Decimal(value_str)
            except Exception:
                return False, _("Nombre decimal attendu")

        elif self.value_type == 'boolean':
            valid_values = ('true', 'false', '1', '0', 'yes', 'no', 'oui', 'non')
            if value_str.lower() not in valid_values:
                return False, _("Valeur booleenne attendue (oui/non, true/false, 1/0)")

        return True, None


# =============================================================================
# Setting
# =============================================================================

class Setting(TranslatableMixin, Timestamped):
    """
    Parametres cle-valeur avec support optionnel d'image.
    Utilise SettingType pour definir le type de valeur et le formatage.
    """
    CATEGORY_CHOICES = [
        ('general', _('General')),
        ('contact', _('Contact')),
        ('social', _('Reseaux sociaux')),
        ('seo', _('SEO')),
        ('appearance', _('Apparence')),
        ('other', _('Autre')),
    ]

    type = models.ForeignKey(
        SettingType,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='settings',
        verbose_name=_("Type")
    )
    category = models.CharField(
        _("Categorie"),
        max_length=20,
        choices=CATEGORY_CHOICES,
        default='general'
    )
    name = models.CharField(_("Nom"), max_length=100)
    slug = models.SlugField(
        _("Cle"),
        max_length=100,
        unique=True,
        blank=True,
        help_text=_("Cle unique pour recuperer ce parametre (auto-genere si vide)")
    )
    value = models.TextField(_("Valeur"), blank=True, null=True)
    image = models.ImageField(
        _("Image"),
        upload_to='cms/settings/',
        blank=True,
        null=True
    )
    description = models.CharField(
        _("Description"),
        max_length=255,
        blank=True,
        help_text=_("Description interne pour l'admin")
    )
    is_required = models.BooleanField(
        _("Obligatoire"),
        default=False,
        help_text=_("Ce parametre doit avoir une valeur")
    )
    is_active = models.BooleanField(_("Actif"), default=True)
    order = models.PositiveIntegerField(_("Ordre"), default=0)

    class Meta:
        verbose_name = _("Parametre")
        verbose_name_plural = _("Parametres")
        ordering = ['category', 'order', 'name']

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def clean(self):
        """Valide selon le type de valeur attendu."""
        # Validation du type requis
        if self.type and self.type.is_image:
            if self.is_required and not self.image:
                raise ValidationError({
                    'image': _("Une image est requise pour ce type de parametre.")
                })
        elif self.is_required and not self.value and not self.image:
            raise ValidationError(
                _("Une valeur ou une image doit etre fournie.")
            )

        # Validation du format selon le type
        if self.type and self.value:
            is_valid, error = self.type.validate_value(self.value)
            if not is_valid:
                raise ValidationError({'value': error})

    @property
    def is_image_setting(self):
        """Retourne True si ce setting est de type image."""
        return self.type and self.type.is_image

    @property
    def value_type(self):
        """Retourne le type de valeur ou 'text' par defaut."""
        if self.type:
            return self.type.value_type
        return 'text'

    def get_value(self):
        """Retourne la valeur formatee selon le type ou l'image en HTML."""
        # Type image
        if self.is_image_setting:
            if self.image:
                return self.type.format_value(self.image.url)
            return ''

        # Type texte/autre
        if self.value:
            if self.type:
                return self.type.format_value(self.value)
            return self.value

        # Fallback image pour type texte sans valeur
        if self.image:
            return format_html(
                '<img src="{}" alt="{}" class="setting-image" />',
                self.image.url,
                self.name
            )
        return ''

    def get_raw_value(self):
        """Retourne la valeur brute sans formatage."""
        if self.is_image_setting and self.image:
            return self.image.url
        return self.value or ''

    def get_typed_value(self):
        """
        Retourne la valeur convertie dans le type Python correspondant.
        Utile pour les calculs et comparaisons.
        """
        if not self.value:
            return None

        value_type = self.value_type

        if value_type == 'boolean':
            return self.value.lower() in ('true', '1', 'yes', 'oui')
        elif value_type == 'number':
            try:
                return int(self.value)
            except ValueError:
                return None
        elif value_type in ('decimal', 'currency', 'percentage'):
            try:
                return Decimal(self.value)
            except Exception:
                return None
        else:
            return self.value

    def get_image_url(self):
        """Retourne l'URL de l'image si presente."""
        if self.image:
            return self.image.url
        return None

    def get_formatted_value(self, custom_pattern=None):
        """
        Retourne la valeur avec un pattern personnalise.

        Args:
            custom_pattern: Pattern optionnel (ex: "<strong>{value}</strong>")
        """
        raw = self.get_raw_value()
        if not raw:
            return ''
        if custom_pattern:
            return custom_pattern.format(value=raw)
        return self.get_value()

    # ==========================================================================
    # Class Methods pour acces rapide
    # ==========================================================================

    @classmethod
    def get(cls, slug, default=None):
        """
        Recupere la valeur brute d'un parametre par son slug.

        Args:
            slug: Cle du parametre
            default: Valeur par defaut si non trouve

        Returns:
            Valeur brute du parametre ou default
        """
        try:
            setting = cls.objects.get(slug=slug, is_active=True)
            return setting.get_raw_value() or default
        except cls.DoesNotExist:
            return default

    @classmethod
    def get_formatted(cls, slug, default='', custom_pattern=None):
        """
        Recupere la valeur formatee d'un parametre par son slug.

        Args:
            slug: Cle du parametre
            default: Valeur par defaut si non trouve
            custom_pattern: Pattern de formatage personnalise

        Returns:
            Valeur formatee du parametre ou default
        """
        try:
            setting = cls.objects.get(slug=slug, is_active=True)
            return setting.get_formatted_value(custom_pattern) or default
        except cls.DoesNotExist:
            return default

    @classmethod
    def get_image(cls, slug):
        """
        Recupere l'URL de l'image d'un parametre.

        Args:
            slug: Cle du parametre

        Returns:
            URL de l'image ou None
        """
        try:
            setting = cls.objects.get(slug=slug, is_active=True)
            return setting.get_image_url()
        except cls.DoesNotExist:
            return None

    @classmethod
    def get_by_category(cls, category):
        """
        Recupere tous les parametres actifs d'une categorie.

        Args:
            category: Code de la categorie (general, contact, social, etc.)

        Returns:
            QuerySet de Settings
        """
        return cls.objects.filter(category=category, is_active=True).order_by('order', 'name')

    @classmethod
    def get_all_as_dict(cls):
        """
        Recupere tous les parametres actifs sous forme de dictionnaire.

        Returns:
            Dict {slug: raw_value}
        """
        settings = cls.objects.filter(is_active=True)
        return {s.slug: s.get_raw_value() for s in settings}

    @classmethod
    def bulk_get(cls, *slugs):
        """
        Recupere plusieurs parametres en une seule requete.

        Args:
            *slugs: Liste des cles a recuperer

        Returns:
            Dict {slug: raw_value}
        """
        settings = cls.objects.filter(slug__in=slugs, is_active=True)
        result = {slug: None for slug in slugs}
        for setting in settings:
            result[setting.slug] = setting.get_raw_value()
        return result


# =============================================================================
# Keyword (Polymorphique N x N)
# =============================================================================

class Keyword(Timestamped):
    """
    Mot-cle unique pouvant etre associe a plusieurs objets de differents modeles.
    Relation N x N polymorphique via KeywordAssignment.
    """
    name = models.CharField(_("Mot-cle"), max_length=100, unique=True)
    slug = models.SlugField(_("Slug"), max_length=100, unique=True, blank=True)
    description = models.TextField(_("Description"), blank=True)

    class Meta:
        verbose_name = _("Mot-cle")
        verbose_name_plural = _("Mots-cles")
        ordering = ['name']

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def get_assigned_objects(self):
        """Retourne tous les objets associes a ce mot-cle."""
        return [assignment.content_object for assignment in self.assignments.all()]

    def get_usage_count(self):
        """Retourne le nombre d'objets utilisant ce mot-cle."""
        return self.assignments.count()

    @classmethod
    def get_or_create_keyword(cls, name):
        """Recupere ou cree un mot-cle par son nom."""
        keyword, created = cls.objects.get_or_create(
            name=name.strip(),
            defaults={'slug': slugify(name.strip())}
        )
        return keyword


class KeywordAssignment(Timestamped):
    """
    Table de jonction pour la relation N x N polymorphique.
    Lie un Keyword a n'importe quel objet.
    """
    keyword = models.ForeignKey(
        Keyword,
        on_delete=models.CASCADE,
        related_name='assignments',
        verbose_name=_("Mot-cle")
    )
    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        verbose_name=_("Type de contenu")
    )
    object_id = models.PositiveIntegerField(_("ID objet"))
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        verbose_name = _("Association mot-cle")
        verbose_name_plural = _("Associations mots-cles")
        ordering = ['keyword__name']
        indexes = [
            models.Index(fields=['content_type', 'object_id']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['keyword', 'content_type', 'object_id'],
                name='unique_keyword_assignment'
            )
        ]

    def __str__(self):
        return f"{self.keyword.name} -> {self.content_object}"

    @classmethod
    def get_keywords_for_object(cls, obj):
        """Recupere tous les mots-cles pour un objet donne."""
        content_type = ContentType.objects.get_for_model(obj)
        assignments = cls.objects.filter(
            content_type=content_type,
            object_id=obj.pk
        ).select_related('keyword')
        return [a.keyword for a in assignments]

    @classmethod
    def add_keyword_to_object(cls, obj, keyword_name):
        """Ajoute un mot-cle a un objet."""
        keyword = Keyword.get_or_create_keyword(keyword_name)
        content_type = ContentType.objects.get_for_model(obj)
        assignment, created = cls.objects.get_or_create(
            keyword=keyword,
            content_type=content_type,
            object_id=obj.pk
        )
        return keyword

    @classmethod
    def remove_keyword_from_object(cls, obj, keyword_name):
        """Supprime un mot-cle d'un objet."""
        content_type = ContentType.objects.get_for_model(obj)
        cls.objects.filter(
            keyword__name=keyword_name,
            content_type=content_type,
            object_id=obj.pk
        ).delete()

    @classmethod
    def set_keywords_for_object(cls, obj, keywords_list):
        """Definit les mots-cles pour un objet (remplace les existants)."""
        content_type = ContentType.objects.get_for_model(obj)
        # Supprimer les anciens
        cls.objects.filter(
            content_type=content_type,
            object_id=obj.pk
        ).delete()
        # Ajouter les nouveaux
        for keyword_name in keywords_list:
            if keyword_name.strip():
                cls.add_keyword_to_object(obj, keyword_name.strip())

    @classmethod
    def get_objects_with_keyword(cls, keyword):
        """Recupere tous les objets ayant un mot-cle specifique."""
        if isinstance(keyword, str):
            try:
                keyword = Keyword.objects.get(name=keyword)
            except Keyword.DoesNotExist:
                return []
        return [a.content_object for a in keyword.assignments.all()]


# =============================================================================
# Button (Bouton reutilisable)
# =============================================================================

class Button(TranslatableMixin, Timestamped, Activatable):
    """
    Bouton reutilisable pouvant etre associe a plusieurs objets.
    Utilise ButtonAssignment pour la relation polymorphique N x N.
    """
    LINK_TYPE_CHOICES = [
        ('external', _('Lien externe')),
        ('internal', _('Page interne')),
        ('url', _('URL nommee')),
    ]

    STYLE_CHOICES = [
        ('primary', _('Primaire (jaune)')),
        ('secondary', _('Secondaire (blanc)')),
        ('outline', _('Contour (transparent)')),
        ('dark', _('Sombre (fond fonce)')),
        ('gradient', _('Degrade')),
        ('ghost', _('Fantome (texte seul)')),
        ('success', _('Succes (vert)')),
        ('danger', _('Danger (rouge)')),
    ]

    name = models.CharField(
        _("Nom interne"),
        max_length=100,
        help_text=_("Nom pour identifier le bouton dans l'admin")
    )
    text = models.CharField(_("Texte du bouton"), max_length=100)
    link_type = models.CharField(
        _("Type de lien"),
        max_length=20,
        choices=LINK_TYPE_CHOICES,
        default='external'
    )
    external_url = models.URLField(
        _("URL externe"),
        blank=True,
        help_text=_("URL complete (ex: https://example.com)")
    )
    internal_page = models.ForeignKey(
        Page,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_("Page interne"),
        help_text=_("Selectionner une page du site")
    )
    named_url = models.CharField(
        _("URL nommee"),
        max_length=100,
        blank=True,
        help_text=_("Nom de l'URL Django (ex: 'contact:contact', 'news:list')")
    )
    style = models.CharField(
        _("Style"),
        max_length=20,
        choices=STYLE_CHOICES,
        default='primary'
    )
    icon = models.ForeignKey(
        'config.Icon',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_("Icone")
    )
    open_in_new_tab = models.BooleanField(_("Ouvrir dans un nouvel onglet"), default=False)

    class Meta:
        verbose_name = _("Bouton")
        verbose_name_plural = _("Boutons")
        ordering = ['name']

    def __str__(self):
        return f"{self.name} ({self.get_translated('text') or self.text})"

    def get_url(self):
        """Retourne l'URL du bouton selon le type de lien."""
        if self.link_type == 'external':
            return self.external_url or '#'
        elif self.link_type == 'internal' and self.internal_page:
            return self.internal_page.get_absolute_url()
        elif self.link_type == 'url' and self.named_url:
            try:
                return reverse(self.named_url)
            except NoReverseMatch:
                return '#'
        return '#'

    def clean(self):
        """Validation du modele."""
        if self.link_type == 'external' and not self.external_url:
            raise ValidationError({'external_url': _('URL externe requise pour un lien externe.')})
        if self.link_type == 'internal' and not self.internal_page:
            raise ValidationError({'internal_page': _('Page interne requise pour un lien interne.')})
        if self.link_type == 'url' and not self.named_url:
            raise ValidationError({'named_url': _('URL nommee requise pour ce type de lien.')})

    def get_usage_count(self):
        """Retourne le nombre d'objets utilisant ce bouton."""
        return self.assignments.count()


class ButtonAssignment(Orderable, Timestamped):
    """
    Table de jonction pour la relation N x N polymorphique.
    Lie un Button a n'importe quel objet avec un ordre specifique.
    """
    button = models.ForeignKey(
        Button,
        on_delete=models.CASCADE,
        related_name='assignments',
        verbose_name=_("Bouton")
    )
    content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        verbose_name=_("Type de contenu")
    )
    object_id = models.PositiveIntegerField(_("ID objet"))
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        verbose_name = _("Association bouton")
        verbose_name_plural = _("Associations boutons")
        ordering = ['order']
        indexes = [
            models.Index(fields=['content_type', 'object_id']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['button', 'content_type', 'object_id'],
                name='unique_button_assignment'
            )
        ]

    def __str__(self):
        return f"{self.button.name} -> {self.content_object}"

    @classmethod
    def get_buttons_for_object(cls, obj, active_only=True):
        """Recupere tous les boutons pour un objet donne."""
        content_type = ContentType.objects.get_for_model(obj)
        qs = cls.objects.filter(
            content_type=content_type,
            object_id=obj.pk
        ).select_related('button', 'button__internal_page', 'button__icon')

        if active_only:
            qs = qs.filter(button__active=True)

        return [a.button for a in qs.order_by('order')]

    @classmethod
    def add_button_to_object(cls, obj, button, order=None):
        """Ajoute un bouton a un objet."""
        content_type = ContentType.objects.get_for_model(obj)
        if order is None:
            max_order = cls.objects.filter(
                content_type=content_type,
                object_id=obj.pk
            ).aggregate(max_order=models.Max('order'))['max_order']
            order = (max_order or 0) + 1

        assignment, created = cls.objects.get_or_create(
            button=button,
            content_type=content_type,
            object_id=obj.pk,
            defaults={'order': order}
        )
        return assignment

    @classmethod
    def remove_button_from_object(cls, obj, button):
        """Supprime un bouton d'un objet."""
        content_type = ContentType.objects.get_for_model(obj)
        cls.objects.filter(
            button=button,
            content_type=content_type,
            object_id=obj.pk
        ).delete()

    @classmethod
    def set_buttons_for_object(cls, obj, buttons_list):
        """Definit les boutons pour un objet (remplace les existants)."""
        content_type = ContentType.objects.get_for_model(obj)
        # Supprimer les anciens
        cls.objects.filter(
            content_type=content_type,
            object_id=obj.pk
        ).delete()
        # Ajouter les nouveaux avec ordre
        for i, button in enumerate(buttons_list):
            cls.add_button_to_object(obj, button, order=i + 1)
