from flask import render_template, redirect, url_for, flash, request, current_app, session
from flask_login import login_required, current_user
import os
import time
import random
import hashlib
import feedparser
import requests
import logging

import re
from datetime import datetime

# Configurar logger estándar para mensajes a nivel de módulo
logger = logging.getLogger(__name__)

# Para convertir Markdown a HTML
try:
    import markdown
    MARKDOWN_AVAILABLE = True
except ImportError:
    MARKDOWN_AVAILABLE = False
    logger.warning("La librería markdown no está disponible. Los artículos en formato markdown se mostrarán sin formato.")
def _looks_like_html(text: str) -> bool:
    try:
        if not text:
            return False
        t = text.lower()
        return any(tag in t for tag in ['<p', '<h1', '<h2', '<h3', '<ul', '<ol', '<li', '<blockquote', '<strong', '<em', '</'])
    except Exception:
        return False

def _maybe_convert_to_html(content: str) -> str:
    """Devuelve HTML: si parece Markdown lo convierte; si ya parece HTML, lo devuelve tal cual."""
    try:
        if _looks_like_html(content):
            return content
        return _convert_markdown_to_html(content)
    except Exception:
        return content
from werkzeug.utils import secure_filename

# Configuración para manejar carga de archivos
def allowed_file(filename):
    """Comprueba si la extensión del archivo está permitida."""
    ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

from pathlib import Path

# Helper para normalizar rutas guardadas en BD (siempre relativas a uploads y con '/')
from typing import Optional
def _normalize_rel_upload_path(p: Optional[str]) -> Optional[str]:
    """Devuelve una ruta relativa y normalizada (con '/') para archivos en uploads.
    Acepta entradas con backslashes, rutas absolutas (Windows/Unix) y con prefijos como
    '/static/uploads/' o '/uploads/'. Devuelve, p.ej., 'blog/img.jpg'.
    """
    if not p:
        return p
    try:
        # Normalizar separadores
        p_norm = str(p).strip().replace('\\', '/')

        # 1) Quitar prefijo absoluto del UPLOAD_FOLDER si está presente
        try:
            base = current_app.config.get('UPLOAD_FOLDER')
        except Exception:
            base = None
        if base:
            base_norm = str(base).replace('\\', '/').rstrip('/')
            # Comparación case-insensitive para Windows
            if p_norm.lower().startswith((base_norm + '/').lower()):
                p_norm = p_norm[len(base_norm) + 1:]

        # 2) Quitar prefijos comunes de static/uploads y uploads si aparecen al inicio o en medio
        prefixes = (
            '/static/uploads/', 'static/uploads/', '/uploads/', 'uploads/'
        )
        for pref in prefixes:
            idx = p_norm.lower().find(pref.lower())
            if idx != -1:
                p_norm = p_norm[idx + len(pref):]
                break

        # 3) Limpiar residuos de rutas relativas
        while p_norm.startswith('/'):
            p_norm = p_norm[1:]
        if p_norm.startswith('./'):
            p_norm = p_norm[2:]

        return p_norm
    except Exception:
        return p

def _abs_upload_path(rel_path: Optional[str]) -> Optional[str]:
    """Convierte 'blog/img.jpg' a ruta absoluta bajo UPLOAD_FOLDER."""
    if not rel_path:
        return None
    rel_path = _normalize_rel_upload_path(rel_path)
    try:
        base = current_app.config.get('UPLOAD_FOLDER')
        if not base:
            return None
        # Evitar backslashes en Windows
        rel_path = rel_path.replace('\\', '/')
        return os.path.join(base, *rel_path.split('/'))
    except Exception:
        return None

from app.extensions import db
from app.models.article import Article, slugify
from . import admin_bp
from .routes import admin_required
from app.forms.admin_forms import ArticleForm, EmptyForm

# apikeys
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
NEWSAPI_KEY = os.environ.get('NEWSAPI_KEY')

# Configurar OpenAI de forma segura para evitar problemas de 'proxies'
# Ya no importamos openai directamente para evitar el error

# Verificar claves de API
if not OPENAI_API_KEY:
    logger.warning("OPENAI_API_KEY no está configurada. La generación de imágenes con DALL-E no funcionará.")

if not NEWSAPI_KEY:
    logger.warning("NEWSAPI_KEY no está configurada. La obtención de noticias internacionales fallará.")

# --------------------------------------------------------------------
# 1. Funciones de apoyo para generación de artículos con IA
# --------------------------------------------------------------------

def _fetch_articles() -> list[dict]:
    """Devuelve artículos con mayor variedad y menos probabilidad de duplicación."""
    articles = []
    temas_energia = [
        "eficiencia energetica", "energia renovable", "energia solar", "energia eolica",
        "hidrogeno verde", "energia nuclear", "biocombustibles", "almacenamiento energia",
        "transicion energetica", "fotovoltaica", "precios electricidad", "mercado energetico",
        "geotermia", "smart grids", "movilidad electrica", "autoconsumo", "certificados verdes",
        "transporte electrico", "renovables marinas", "economia circular", "redes inteligentes", 
        "aislamiento termico", "comunidades energeticas", "vehiculo electrico", "redes de calor",
        "biomasa", "certificado energetico", "cero emisiones", "descarbonizacion", "hidrogeno azul"
    ]
    
    # Palabras clave para reforzar el dominio energético y excluir automoción genérica
    energy_keywords = [
        "energia", "energético", "energetico", "renovable", "renovables", "solar", "fotovoltaica",
        "eolica", "eólica", "hidrogeno", "hidrógeno", "biomasa", "geotermia", "autoconsumo",
        "redes inteligentes", "smart grid", "smart grids", "almacenamiento", "batería", "bateria",
        "precio de la luz", "tarifa", "mercado electr", "mix energ", "kwh", "mw", "gw", "subestación",
        "subestacion", "red eléctrica", "red electrica", "ciclo combinado", "nuclear", "hidráulica",
        "hidraulica", "eeb", "cnmc", "ree", "operador del sistema", "peaje", "curtailment", "ppa",
    ]
    negative_auto_keywords = [
        "prueba de", "prueba: ", "review", "test drive", "conducción", "conduccion", "motor",
        "gasolina", "diesel", "diésel", "berlina", "suv", "coupe", "cupé", "hatchback", "roadster",
        "acabado", "acabados", "equipamiento", "version", "versión", "modelo", "modelo 20",
        "caballos", "cv", "0-100", "0 a 100", "velocidad máxima", "velocidad maxima", "par motor",
        "consumo wlpt", "homologación", "homologacion", "tracción", "traccion", "llantas",
        "autobild", "motorpasion", "topgear", "autopista", "carscoops", "caranddriver",
    ]
    exclude_domains = [
        "motorpasion", "autobild", "autopista", "carscoops", "caranddriver",
        "marca.com/motor", "as.com/motor", "mundodeportivo.com/motor", "motor1", "hibridosyelectricos",
        "topgear", "forocoches"
    ]
    
    # Crear una semilla única usando timestamp y un valor aleatorio
    semilla = int(time.time()) + random.randint(1, 100000)
    random.seed(semilla)
    logger.info(f"Usando semilla aleatoria para selección de artículos: {semilla}")
    
    # Seleccionar tres temas diferentes para mayor variedad
    temas_seleccionados = random.sample(temas_energia, 3)
    
    # Lista de fuentes RSS de energía en español
    fuentes = [
        {'url': 'https://elperiodicodelaenergia.com/feed/', 'name': 'El Periódico de la Energía (España)'},
        {'url': 'https://www.energias-renovables.com/rss/feed', 'name': 'Energías Renovables'},
        {'url': 'https://www.eseficiencia.es/feed/', 'name': 'Eseficiencia'},
        {'url': 'https://solar-energia.net/feed/', 'name': 'Solar Energía'},
        {'url': 'https://www.energynews.es/feed/', 'name': 'Energy News'},
        {'url': 'https://www.pv-magazine.es/feed/', 'name': 'PV Magazine'},
        {'url': 'https://www.energetica21.com/rss', 'name': 'Energética21'}
    ]
    
    # Mezclar las fuentes para variar el orden
    random.shuffle(fuentes)
    
    # Obtener artículos de diferentes fuentes
    fuentes_utilizadas = []
    for fuente in fuentes:
        if len(articles) >= 2:  # Limitar a 2 artículos como máximo
            break
        
        # Evitar repetir la misma fuente
        if fuente['name'] in fuentes_utilizadas:
            continue
        
        try:
            # Seleccionar tema para esta fuente
            tema_seleccionado = temas_seleccionados[len(articles) % len(temas_seleccionados)]
            
            logger.debug(f"Obteniendo feed de {fuente['name']} para tema {tema_seleccionado}")
            feed = feedparser.parse(fuente['url'])
            
            if not feed.entries:
                logger.warning(f"No hay entradas en el feed de {fuente['name']}")
                continue
            
            # Usar índices aleatorios para evitar siempre obtener los mismos artículos
            max_index = min(10, len(feed.entries))
            start_index = random.randint(0, max(0, max_index - 1))
            
            # Revisar hasta 5 artículos aleatorios de esta fuente
            article_found = False
            for i in range(min(5, max_index)):
                index = (start_index + i) % max_index
                entry = feed.entries[index]
                
                # Verificar que el artículo no esté ya en la lista por título
                title_exists = any(a['title'] == entry.title for a in articles)
                if title_exists:
                    continue
                
                # Filtrado por temática: positivo energía y negativo automoción
                summary = getattr(entry, 'summary', '') or ''
                text_lc = f"{entry.title} {summary}".lower()
                link_lc = (entry.link or '').lower()
                if any(dom in link_lc for dom in exclude_domains):
                    logger.debug(f"Descartado por dominio de motor: {entry.link}")
                    continue
                if not (tema_seleccionado in text_lc or any(k in text_lc for k in energy_keywords)):
                    logger.debug(f"Descartado por no contener señales de energía para tema '{tema_seleccionado}': {entry.title}")
                    continue
                if any(k in text_lc for k in negative_auto_keywords):
                    logger.debug(f"Descartado por clave negativa de automoción: {entry.title}")
                    continue
                
                # Añadir este artículo si pasa filtros
                articles.append({
                    'title': entry.title,
                    'description': summary[:500],
                    'url': entry.link,
                    'source': fuente['name'],
                    'image': None,
                    'tema': tema_seleccionado
                })
                fuentes_utilizadas.append(fuente['name'])
                article_found = True
                break
            
            if not article_found:
                logger.warning(f"No se encontró un artículo adecuado en {fuente['name']}")
        except Exception as e:
            logger.error(f"Error procesando fuente {fuente['name']}: {str(e)}")
    
    # Si no tenemos al menos 1 artículo, intentar con NewsAPI como respaldo
    if len(articles) < 1 and NEWSAPI_KEY:
        try:
            # Intentar con NewsAPI como alternativa
            tema_respaldo = random.choice(temas_energia)
            logger.info(f"Intentando obtener artículo de respaldo con NewsAPI para tema: {tema_respaldo}")
            
            # Nota: NewsAPI no soporta booleanos complejos en 'q' en todos los planes; reforzamos con post-filtrado
            newsapi_url = (
                f"https://newsapi.org/v2/everything?q={tema_respaldo}&language=es&sortBy=publishedAt&pageSize=10&apiKey={NEWSAPI_KEY}"
            )
            response = requests.get(newsapi_url, timeout=10)
            
            if response.status_code == 200:
                data = response.json()
                if data.get('articles') and len(data['articles']) > 0:
                    # Revisar hasta 10 candidatos con post-filtrado de dominio/temática
                    candidates = data['articles'][:min(10, len(data['articles']))]
                    indices = list(range(len(candidates)))
                    random.shuffle(indices)
                    picked = False
                    for idx in indices:
                        article = candidates[idx]
                        title = article.get('title', '') or ''
                        desc = article.get('description', '') or ''
                        url = article.get('url', '') or ''
                        source_name = article.get('source', {}).get('name', 'Fuente desconocida')
                        text_lc = f"{title} {desc}".lower()
                        link_lc = url.lower()
                        if any(dom in link_lc for dom in exclude_domains):
                            logger.debug(f"[NewsAPI] Descartado por dominio de motor: {url}")
                            continue
                        if not (tema_respaldo in text_lc or any(k in text_lc for k in energy_keywords)):
                            logger.debug(f"[NewsAPI] Descartado por no ser claramente de energía: {title}")
                            continue
                        if any(k in text_lc for k in negative_auto_keywords):
                            logger.debug(f"[NewsAPI] Descartado por clave negativa de automoción: {title}")
                            continue
                        articles.append({
                            'title': title if title else 'Sin título',
                            'description': desc[:500],
                            'url': url if url else '#',
                            'source': f"NewsAPI - {source_name}",
                            'image': article.get('urlToImage'),
                            'tema': tema_respaldo
                        })
                        picked = True
                        break
                    if not picked:
                        logger.warning("NewsAPI no proporcionó artículos que pasen el filtro de energía")
                else:
                    logger.warning("NewsAPI no devuelve artículos para este tema")
            else:
                logger.warning(f"Error en solicitud a NewsAPI: {response.status_code}")
        except Exception as e:
            logger.error(f"Error con NewsAPI: {e}")
    
    # Registrar resultados
    logger.info(f"Artículos obtenidos: {len(articles)} de {len(fuentes_utilizadas)} fuentes")
    for i, article in enumerate(articles):
        logger.info(f"Artículo {i+1}: {article['title']} - Fuente: {article['source']} - Tema: {article['tema']}")
    
    return articles[:2]  # Máximo 2 artículos (1 nacional + 1 internacional)


from typing import Optional

def _download_and_save_image(img_url: str, title: str) -> Optional[str]:
    """Descarga imagen y la guarda en static/uploads/blog; devuelve ruta relativa."""
    if not img_url:
        logger.warning("URL de imagen vacía")
        return None
        
    try:
        logger.info(f"Descargando imagen: {img_url}")
        headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
        resp = requests.get(img_url, timeout=15, headers=headers, allow_redirects=True)
        resp.raise_for_status()
        
        # Verificar que el contenido es una imagen
        content_type = resp.headers.get('content-type', '')
        if not content_type.startswith('image/'):
            logger.warning(f"El recurso no es una imagen: {content_type}")
            return None
            
        folder = Path(current_app.config['UPLOAD_FOLDER']) / 'blog'
        folder.mkdir(parents=True, exist_ok=True)
        
        # Determinar la extensión basada en el content-type
        extension = 'jpg'  # por defecto
        if 'image/png' in content_type:
            extension = 'png'
        elif 'image/gif' in content_type:
            extension = 'gif'
        elif 'image/webp' in content_type:
            extension = 'webp'
            
        filename = secure_filename(f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}_{title[:30]}.{extension}")
        file_path = folder / filename
        
        with open(file_path, 'wb') as f:
            f.write(resp.content)
            
        logger.info(f"Imagen guardada: {file_path}")
        # Devolver siempre ruta relativa con '/'
        return f"blog/{filename}"
    except requests.exceptions.RequestException as e:
        logger.warning(f"Error en solicitud HTTP al descargar imagen: {e}")
        return None
    except Exception as e:
        logger.warning(f"Error descargando imagen: {e}")
        return None


def _convert_markdown_to_html(content: str) -> str:
    """Convierte contenido en formato Markdown a HTML."""
    if content is None:
        return ""
    
    # Nota: No escapamos etiquetas HTML antes de convertir Markdown para evitar doble escape.
    # Solo registramos un aviso si el modelo devolvió HTML incrustado en lugar de Markdown puro.
    try:
        import re
        common_html_tags = ['<h1', '<h2', '<h3', '<p', '<strong', '<em', '<ul', '<li', '<ol', '<blockquote']
        if any(tag in content.lower() for tag in common_html_tags):
            logger.debug("Contenido incluye HTML; se convertirá Markdown sin pre-escapar.")
    except Exception as e:
        logger.debug(f"Omisión de preprocesado HTML por excepción menor: {e}")
    
    # Si la librería markdown no está disponible, usar conversión básica
    if not MARKDOWN_AVAILABLE:
        logger.warning("Intentando convertir markdown a HTML sin la librería markdown. Usará conversión básica.")
        # Implementación básica de conversión para casos donde no está disponible la librería
        # Encabezados
        for i in range(3, 0, -1):
            tag = "h" + str(i)
            content = content.replace("#" * i + " ", f"<{tag}>") + f"</{tag}>"
        
        # Negritas e itálicas
        content = content.replace("**", "<strong>").replace("**", "</strong>")
        content = content.replace("*", "<em>").replace("*", "</em>")
        
        # Mantener saltos de línea
        content = content.replace("\n", "<br>")
        
        return content
    
    try:
        # Usar librería markdown con extensiones comunes
        # Intentar con un conjunto de extensiones que debería funcionar en la mayoría de versiones
        try:
            html = markdown.markdown(
                content, 
                extensions=['extra', 'tables', 'nl2br'],
                output_format='html5'
            )
        except Exception as e:
            logger.warning(f"Error con extensiones completas: {e}. Intentando con extensiones básicas.")
            # Si falla, intentar con menos extensiones
            html = markdown.markdown(content, extensions=['tables', 'nl2br'])
        
        logger.info("Contenido markdown convertido exitosamente a HTML")
        return html
    except Exception as e:
        logger.error(f"Error al convertir markdown a HTML: {e}")
        # En caso de error, mantener el contenido original pero escapar HTML
        return content.replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>")


def _trim_overlap(prev_text: str, new_text: str, min_overlap_words: int = 20, max_window_words: int = 120) -> str:
    """Elimina solapamiento si el inicio de `new_text` repite el final de `prev_text`.
    Comparación por palabras. Si detecta un solapamiento significativo (>= min_overlap_words),
    recorta la parte duplicada del comienzo de `new_text` y devuelve el resto.
    """
    try:
        if not prev_text or not new_text:
            return new_text
        prev_words = prev_text.split()
        new_words = new_text.split()
        if not prev_words or not new_words:
            return new_text

        # Ventanas de comparación para evitar coste O(n^2)
        tail = prev_words[-max_window_words:]
        head = new_words[:max_window_words]

        max_k = min(len(tail), len(head))
        overlap = 0
        # Buscar el mayor k tal que tail[-k:] == head[:k]
        for k in range(max_k, min_overlap_words - 1, -1):
            if tail[-k:] == head[:k]:
                overlap = k
                break

        if overlap >= min_overlap_words:
            return " ".join(new_words[overlap:]).lstrip()
        return new_text
    except Exception:
        # Si algo falla, no recortar
        return new_text


def _normalize_sentence(s: str) -> str:
    """Normaliza una frase: minúsculas, sin múltiple espacio, sin espacios en extremos."""
    try:
        s = re.sub(r"\s+", " ", s or "").strip().lower()
        return s
    except Exception:
        return s or ""


def _split_sentences(text: str) -> list[str]:
    """Divide el texto en frases usando puntuación común. Conservador para español."""
    try:
        # Separar por punto, admiración o interrogación, manteniendo cortes limpios
        parts = re.split(r"(?<=[\.!\?])\s+", text or "")
        # Filtrar vacíos
        return [p.strip() for p in parts if p and p.strip()]
    except Exception:
        return [text] if text else []


def _dedupe_sentences(prev_text: str, new_text: str, similarity_threshold: float = 0.92) -> tuple[str, int]:
    """Elimina de new_text las frases casi duplicadas respecto a prev_text.
    Devuelve (texto_filtrado, cantidad_frases_eliminadas).
    """
    try:
        import difflib
        prev_sentences_raw = _split_sentences(prev_text)
        new_sentences_raw = _split_sentences(new_text)

        prev_norm = [_normalize_sentence(s) for s in prev_sentences_raw]
        kept = []
        removed = 0

        for s in new_sentences_raw:
            s_norm = _normalize_sentence(s)
            if not s_norm:
                continue
            # Exacto
            if s_norm in prev_norm:
                removed += 1
                continue
            # Similaridad alta con alguna frase previa
            is_dup = False
            for p in prev_norm[-200:]:  # limitar a últimas 200 frases para rendimiento
                if not p:
                    continue
                ratio = difflib.SequenceMatcher(None, p, s_norm).ratio()
                if ratio >= similarity_threshold:
                    is_dup = True
                    break
            if is_dup:
                removed += 1
            else:
                kept.append(s)

        return (" ".join(kept).strip(), removed)
    except Exception:
        return new_text, 0


def _dedupe_paragraphs(text: str) -> tuple[str, int]:
    """Elimina párrafos exactos repetidos (normalizados).
    Conservador: solo exactos, para no perder contenido válido.
    Devuelve (texto_filtrado, cantidad_parrafos_eliminados).
    """
    try:
        paragraphs = [p for p in re.split(r"\n\n+", text or "") if p and p.strip()]
        seen = set()
        kept = []
        removed = 0
        for p in paragraphs:
            key = _normalize_sentence(p)
            if key in seen:
                removed += 1
                continue
            seen.add(key)
            kept.append(p)
        return ("\n\n".join(kept), removed)
    except Exception:
        return text, 0


def _parse_h2_sections(md: str):
    """Devuelve una lista de secciones H2 como tuplas (heading_line, title_lower, content).
    Incluye solo secciones que empiezan por '## '. También devuelve el prefijo antes del primer H2.
    """
    try:
        lines = (md or "").splitlines()
        sections = []
        prefix_lines = []
        current_heading = None
        current_title_lower = None
        buffer = []

        for line in lines:
            if line.startswith("## "):
                if current_heading is None and buffer:
                    prefix_lines = buffer[:]
                if current_heading is not None:
                    sections.append((current_heading, current_title_lower, "\n".join(buffer).strip()))
                current_heading = line.rstrip()
                current_title_lower = line[3:].strip().lower()
                buffer = []
            else:
                buffer.append(line)

        if current_heading is not None:
            sections.append((current_heading, current_title_lower, "\n".join(buffer).strip()))
        else:
            # No hubo H2; todo es prefijo
            prefix_lines = buffer[:]

        prefix = "\n".join(prefix_lines).strip()
        return prefix, sections
    except Exception:
        return (md or ""), []


def _estimate_body_word_count(md: str) -> int:
    """Cuenta palabras en secciones H2 que NO sean conclusión/cierre.
    Si no hay H2, devuelve 0 (para forzar expansión de cuerpo estructurado).
    """
    try:
        _, sections = _parse_h2_sections(md)
        if not sections:
            return 0
        total = 0
        for heading, title_lower, content in sections:
            t = (title_lower or "")
            if any(k in t for k in ["conclus", "cierre", "final"]):
                continue
            total += len((content or "").split())
        return total
    except Exception:
        return 0


def _truncate_conclusion(md: str, max_words: int = 140) -> str:
    """Trunca la sección de conclusión/cierre a un máximo de palabras, si existe.
    Conserva el resto del contenido intacto.
    """
    try:
        prefix, sections = _parse_h2_sections(md)
        if not sections:
            return md
        out_parts = []
        if prefix:
            out_parts.append(prefix)
        for heading, title_lower, content in sections:
            out_parts.append(heading)
            t = (title_lower or "")
            if any(k in t for k in ["conclus", "cierre", "final"]):
                words = (content or "").split()
                if len(words) > max_words:
                    content = " ".join(words[:max_words]).rstrip() + "\n"
            if content:
                out_parts.append(content)
        # Asegurar saltos de línea entre bloques
        return "\n\n".join([p for p in out_parts if p is not None])
    except Exception:
        return md

def _generate_article_body(prompt: str) -> str:
    """Genera el contenido del artículo utilizando OpenAI GPT-4o con objetivo 1500-2000 palabras.
    Exige mínimo de 900 palabras SOLO en el CUERPO PRINCIPAL (H2) y mantiene la conclusión breve.
    """
    if not OPENAI_API_KEY:
        logger.error("La clave API de OpenAI no está configurada. Revise la variable de entorno OPENAI_API_KEY.")
        return "[Contenido no disponible - Falta OpenAI API Key]"

    # -----------------------------
    # Configuración y constantes
    # -----------------------------
    BODY_MIN, BODY_MAX = 1500, 2000  # solo informativo para el prompt externo
    MIN_WORDS = 950                 # Umbral de aceptación real (ajustado)
    MIN_BODY_WORDS = 900            # Palabras mínimas en el CUERPO PRINCIPAL (H2)
    MAX_CONCLUSION_WORDS = 140      # Tope de palabras para la conclusión
    MAX_RETRIES = 5                 # Máximo de intentos

    url = "https://api.openai.com/v1/chat/completions"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {OPENAI_API_KEY}"
    }

    # Conversación inicial
    system_msg = {
        "role": "system",
        "content": (
            "Eres un periodista especializado en energía y tecnología que escribe artículos detallados "
            "para un blog empresarial en español. Creas contenido estructurado y original siguiendo las "
            "mejores prácticas SEO. "
            "Sigue esta distribución estricta: Bajada 50-80 palabras; Lead 120-180 palabras; "
            "Cuerpo principal (H2) mínimo 900 palabras en total, repartidas en 3-4 H2 bien desarrollados; "
            "Conclusión entre 90 y 140 palabras. No escribas la conclusión hasta el final."
        )
    }
    
    # Acumulador de contenido
    full_content = ""
    attempt = 0
    
    while attempt < MAX_RETRIES and (len(full_content.split()) < MIN_WORDS or _estimate_body_word_count(full_content) < MIN_BODY_WORDS):
        try:
            # Crear nuevo mensaje según si es primer intento o continuación
            if attempt == 0:
                user_msg = {"role": "user", "content": prompt}
            else:
                deficit_total = max(0, MIN_WORDS - len(full_content.split()))
                body_words_so_far = _estimate_body_word_count(full_content)
                deficit_body = max(0, MIN_BODY_WORDS - body_words_so_far)
                tail_words = " ".join(full_content.split()[-60:])
                continuation_msg = (
                    "CONTINÚA el artículo EXACTAMENTE desde el punto donde terminó sin repetir. "
                    f"Prioriza ampliar el CUERPO PRINCIPAL en H2 (faltan ≈ {deficit_body} palabras en el cuerpo) "
                    f"y luego el total (faltan ≈ {deficit_total} palabras totales si aplica). "
                    "No reescribas la bajada ni el lead, no reinicies H2 ya usados y no repitas párrafos previos. "
                    "No escribas ni alargues la CONCLUSIÓN todavía hasta que el cuerpo alcance el mínimo. "
                    "Usa Markdown puro (sin etiquetas HTML). "
                    f"Últimas 60 palabras ya generadas (contexto, NO repitas): «{tail_words}»."
                )
                user_msg = {"role": "user", "content": continuation_msg}
            
            # Componer mensajes para esta solicitud
            messages = [system_msg]
            if attempt > 0:
                # Añadir el contenido actual como "mensaje" del asistente
                messages.append({"role": "assistant", "content": full_content})
            messages.append(user_msg)

            payload = {
                "model": "gpt-4o",
                "messages": messages,
                "temperature": 0.7,
                "max_tokens": 4000,  # GPT-4o soporta respuestas más largas
                "frequency_penalty": 0.25,
                "presence_penalty": 0.0
            }

            logger.info(f"OpenAI GPT-4o intento {attempt + 1}/{MAX_RETRIES}")
            resp = requests.post(url, headers=headers, json=payload, timeout=120)
            resp.raise_for_status()
            data = resp.json()
            
            # Obtener el nuevo contenido generado
            new_content = data["choices"][0]["message"]["content"].strip()
            word_count_new = len(new_content.split())
            logger.info(f"GPT-4o devolvió {word_count_new} palabras")
            
            # En primera iteración, usar tal cual; en continuaciones, concatenar con recorte de solapamiento
            if attempt == 0:
                full_content = new_content
            else:
                trimmed = _trim_overlap(full_content, new_content)
                if len(trimmed.split()) < len(new_content.split()):
                    logger.debug(f"Recorte por solapamiento: -{len(new_content.split()) - len(trimmed.split())} palabras")
                deduped, removed_sent = _dedupe_sentences(full_content, trimmed)
                if removed_sent:
                    logger.debug(f"Frases eliminadas por similitud: {removed_sent}")
                full_content += "\n\n" + deduped
            
            # Contabilizar palabras acumuladas
            total_words = len(full_content.split())
            body_words = _estimate_body_word_count(full_content)
            logger.info(f"Contenido acumulado: {total_words} palabras | Cuerpo principal: {body_words} palabras")
            
            if total_words >= MIN_WORDS and body_words >= MIN_BODY_WORDS:
                # Limpieza final de párrafos duplicados exactos
                cleaned, removed_pars = _dedupe_paragraphs(full_content)
                if removed_pars:
                    logger.debug(f"Párrafos duplicados eliminados al final: {removed_pars}")
                # Truncar conclusión si excede el máximo
                cleaned = _truncate_conclusion(cleaned, MAX_CONCLUSION_WORDS)
                logger.info("Requisitos mínimos alcanzados (total y cuerpo). Devolviendo artículo (Markdown)")
                return cleaned
                
            # Continuamos con siguiente intento
            attempt += 1
            if attempt < MAX_RETRIES:
                missing_total = max(0, MIN_WORDS - total_words)
                missing_body = max(0, MIN_BODY_WORDS - body_words)
                logger.warning(
                    f"Artículo corto (total={total_words}, cuerpo={body_words}). "
                    f"Faltan: total≈{missing_total}, cuerpo≈{missing_body}. Pidiendo CONTINÚA…"
                )
                time.sleep(3)
                
        except requests.exceptions.RequestException as e:
            logger.error(f"Error HTTP/Red con OpenAI: {e}")
            attempt += 1
            time.sleep(3)
        except (KeyError, IndexError) as e:
            logger.error(f"Formato inesperado de respuesta OpenAI: {e}")
            attempt += 1
            time.sleep(3)
        except Exception as e:
            logger.error(f"Error inesperado con OpenAI: {e}")
            attempt += 1
            time.sleep(3)

    # Si llegamos hasta aquí pero tenemos algo de contenido, lo devolvemos aunque sea corto
    if full_content and len(full_content.split()) > 400:
        logger.warning(
            f"Contenido parcial: total={len(full_content.split())} palabras, cuerpo={_estimate_body_word_count(full_content)} palabras."
        )
        # Truncar conclusión en cualquier caso para evitar que domine el texto
        return _truncate_conclusion(full_content, MAX_CONCLUSION_WORDS)
    
    # Todos los intentos agotados sin contenido útil
    logger.error("GPT-4o no devolvió contenido válido tras varios intentos")
    return "[Error: No se pudo generar un artículo completo]"


def _get_min_content_words():
    """Devuelve el número mínimo de palabras para un artículo."""
    # Ajustado para artículos más largos
    return 1000


def _generate_featured_image(title: str, article_content: Optional[str] = None) -> Optional[str]:
    """
    Busca una foto horizontal en Freepik y la guarda localmente.
    Devuelve la ruta relativa a /static o None si falla.
    """
    logger = logging.getLogger(__name__)
    # Usar la clave API de Freepik desde variable de entorno
    FREEPIK_API_KEY = os.getenv("FREEPIK_API_KEY", "").strip().strip("'\"")

    if not FREEPIK_API_KEY:
        logger.error("Falta FREEPIK_API_KEY")
        return None
    else:
        try:
            masked = (FREEPIK_API_KEY[:4] + "..." + FREEPIK_API_KEY[-4:]) if len(FREEPIK_API_KEY) >= 8 else "(muy corta)"
            logger.debug(f"FREEPIK_API_KEY cargada (enmascarada): {masked}")
        except Exception:
            pass

    # -------- 1. PREPARAR BÚSQUEDA --------
    #   → 5 palabras clave (=mejor "recall")
    base_kw = re.sub(r"[^\w\s]", " ", (title or "")).lower().split()
    if article_content:
        base_kw += re.sub(r"[^\w\s]", " ", article_content[:120]).lower().split()
    search_terms = " ".join([w for w in base_kw if len(w) > 3][:5]) or "energía renovable"

    params = {
        "term": search_terms,
        "limit": 20,
        "order": "relevance",
        "type": "photo",
        "filters[content_type][photo]": 1,
        "filters[orientation][landscape]": 1,
        # Solicitamos dimensiones específicas para la imagen
        "filters[width][min]": 1792,
        "filters[height][min]": 1024
    }
    headers = {
        "x-freepik-api-key": FREEPIK_API_KEY,  # ✔ cabecera correcta
        "Accept-Language": "es-ES"
    }

    try:
        # -------- 2. BUSCAR RECURSOS --------
        r = requests.get("https://api.freepik.com/v1/resources", params=params, headers=headers, timeout=20)
        r.raise_for_status()
        data = r.json()

        if not data.get("data"):
            logger.warning("Sin resultados Freepik")
            return None

        # Primera coincidencia
        resource = data["data"][0]
        res_id = resource["id"]
        
        # Dump completo de todas las claves del recurso para debug
        logger.debug(f"ID del recurso seleccionado: {res_id}")
        logger.debug(f"Claves del recurso: {list(resource.keys())}")
        logger.debug(f"Tipo de contenido: {resource.get('content_type')}")
        
        # Ver si hay URLs en los campos esperados
        if 'preview' in resource:
            logger.debug(f"Campos en preview: {list(resource['preview'].keys() if isinstance(resource['preview'], dict) else [])}")
        if 'thumbnail' in resource:
            logger.debug(f"Campos en thumbnail: {list(resource['thumbnail'].keys() if isinstance(resource['thumbnail'], dict) else [])}")
        if 'image' in resource:
            logger.debug(f"Campos en image: {list(resource['image'].keys() if isinstance(resource['image'], dict) else [])}")

        # -------- 3. DESCARGAR DIRECTAMENTE DESDE LA MINIATURA --------
        # En lugar de usar el endpoint /download, usamos la URL de la miniatura de alta calidad
        # que viene directamente en los metadatos del recurso
        
        logger.debug(f"Analizando recurso: {resource}")
        
        # Extraer de forma robusta URLs de imagen desde los metadatos
        def _collect_urls(obj, parent_keys=None):
            if parent_keys is None:
                parent_keys = []
            urls = []
            if isinstance(obj, dict):
                for k, v in obj.items():
                    new_keys = parent_keys + [str(k).lower()]
                    urls += _collect_urls(v, new_keys)
            elif isinstance(obj, list):
                for v in obj:
                    urls += _collect_urls(v, parent_keys)
            elif isinstance(obj, str):
                if obj.startswith("http"):
                    key_path = ".".join(parent_keys)
                    urls.append((key_path, obj))
            return urls

        candidates = []
        scope_objs = [resource]
        if isinstance(resource, dict) and "assets" in resource and isinstance(resource["assets"], dict):
            scope_objs.insert(0, resource["assets"])  # priorizar assets si existe

        for scope in scope_objs:
            candidates.extend(_collect_urls(scope))

        def _score(item):
            key_path, url = item
            score = 0
            for kw in ["large", "1000", "big", "original", "preview", "image", "url"]:
                if kw in key_path:
                    score += 2
            for kw in ["thumbnail", "small", "200", "150"]:
                if kw in key_path:
                    score -= 1
            if url.endswith(".jpg") or url.endswith(".jpeg") or "jpg" in url:
                score += 1
            if "svg" in url or "vector" in key_path:
                score -= 2
            return score

        candidates = sorted(candidates, key=_score, reverse=True)

        img_url = None
        for key_path, url in candidates:
            if url.startswith("http"):
                img_url = url
                logger.debug(f"Candidata: {key_path} -> {url}")
                break

        if not img_url:
            logger.error("No se encontraron URLs válidas en el recurso")
            return None

        # Descargar imagen con headers adecuados y validar que sea imagen
        img_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
        try:
            img_resp = requests.get(img_url, headers=img_headers, timeout=20, allow_redirects=True)
            img_resp.raise_for_status()
            ctype = img_resp.headers.get("content-type", "")
            if not ctype.startswith("image/"):
                logger.warning(f"URL obtenida no devuelve imagen (content-type={ctype}). Abortando.")
                return None
            img_bytes = img_resp.content
        except Exception as e:
            logger.error(f"No fue posible descargar la imagen directa: {e}")
            return None
        
        # Procesar la imagen para asegurar las dimensiones correctas
        try:
            from PIL import Image
            import io
            
            # Cargar imagen desde bytes
            image_data = io.BytesIO(img_bytes)
            pil_img = Image.open(image_data)
            
            # Para garantizar que la imagen esté centrada, usamos un enfoque diferente
            # Primero, aseguramos que la imagen sea lo suficientemente grande
            if pil_img.width < 1792 or pil_img.height < 1024:
                # Si es demasiado pequeña, la ampliamos preservando la proporción
                ratio = max(1792 / pil_img.width, 1024 / pil_img.height)
                new_width = int(pil_img.width * ratio)
                new_height = int(pil_img.height * ratio)
                pil_img = pil_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                
            # Ahora recortamos desde el centro para obtener exactamente 1792x1024
            left = (pil_img.width - 1792) // 2
            top = (pil_img.height - 1024) // 2
            right = left + 1792
            bottom = top + 1024
            
            # Aseguramos que no nos salimos de los límites
            if left < 0: left = 0
            if top < 0: top = 0
            if right > pil_img.width: right = pil_img.width
            if bottom > pil_img.height: bottom = pil_img.height
            
            # Recortamos la imagen para centrarla
            pil_img = pil_img.crop((left, top, right, bottom))
            
            # Si después del recorte no tenemos las dimensiones exactas, rellenamos con blanco
            if pil_img.width != 1792 or pil_img.height != 1024:
                new_img = Image.new("RGB", (1792, 1024), (255, 255, 255))
                paste_x = (1792 - pil_img.width) // 2
                paste_y = (1024 - pil_img.height) // 2
                new_img.paste(pil_img, (paste_x, paste_y))
                pil_img = new_img
            
            # Convertir de vuelta a bytes
            img_byte_arr = io.BytesIO()
            # Guardar con alta calidad (95%) y sin compresión adicional
            pil_img.save(img_byte_arr, format='JPEG', quality=95, optimize=False, 
                        subsampling=0, progressive=True)
            img_bytes = img_byte_arr.getvalue()
            
            logger.debug(f"Imagen procesada a dimensiones: {pil_img.width}x{pil_img.height}")
        except ImportError:
            logger.warning("No se pudo importar PIL. La imagen no será redimensionada.")
        except Exception as e:
            logger.warning(f"Error al procesar imagen: {e}. Usando imagen original.")

        # -------- 4. GUARDAR LOCAL --------
        folder = Path(current_app.config['UPLOAD_FOLDER']) / 'blog'
        folder.mkdir(parents=True, exist_ok=True)
        fname = secure_filename(f"freepik_{datetime.utcnow():%Y%m%d%H%M%S}_{title[:30]}.jpg")
        fpath = folder / fname
        with open(fpath, "wb") as f:
            f.write(img_bytes)
        logger.info(f"Imagen Freepik guardada: {fpath}")
        return f"blog/{fname}"

    except requests.exceptions.HTTPError as e:
        logger.error(f"HTTP Freepik {e.response.status_code}: {e.response.text[:150]}")
    except Exception as e:
        logger.error(f"Error Freepik: {e}")

    return None


# --------------------------------------------------------------------
# Generación de contenidos con IA
# --------------------------------------------------------------------

def _generate_random_energy_title():
    """
    Genera un título aleatorio relacionado con energía renovable.
    """
    import random
    
    prefijos = [
        "El futuro de", "Avances en", "Innovaciones en", "La revolución de", 
        "Tendencias en", "Desarrollos recientes en", "El impacto de", 
        "La transformación mediante", "Oportunidades en", "El potencial de",
        "Cómo aprovechar", "Desafíos y soluciones en", "Perspectivas sobre",
        "Estrategias para implementar", "Beneficios de", "Optimizando"
    ]
    
    temas = [
        "la energía solar", "la energía eólica", "la biomasa", "la energía hidroeléctrica",
        "la energía geotérmica", "el hidrógeno verde", "los paneles solares de última generación",
        "los aerogeneradores offshore", "el almacenamiento de energía", "las baterías de iones de litio",
        "las microrredes inteligentes", "la energía renovable en entornos urbanos",
        "la eficiencia energética", "las ciudades sostenibles", "la movilidad eléctrica",
        "la descarbonización industrial", "las comunidades energéticas", "el autoconsumo energético", 
        "Noticas de comecializadoras de energía en Europa", "Noticas de comecializadoras de energía en España",
        "Ventajas de los coches electricos", "Problemas con las baterias de los coches electricos",
    ]
    
    contextos = [
        "en la transición energética", "en la lucha contra el cambio climático", 
        "para un futuro sostenible", "en la economía verde", "en el sector industrial",
        "en España", "en Europa", "a nivel global", "en países en desarrollo",
        "para las empresas", "para los consumidores", "para la industria 4.0",
        "cambio climatico", "y los Objetivos de Desarrollo Sostenible", 
        "en la era post-carbono", "energia", "energia verde", "energia solar", 
        "energia eólica", "energia geotérmica", "energia hidroeléctrica", 
        "energia biomasa", "energia hidroeléctrica", "Futuras energias renovables",
        "energia nuclear", "energia sostenible", "energia geoterminca", "Experimentos para nuevas fuentes de energia", 
        "España en innovacion en nuevas fuentes de energia",
    ]
    
    # A veces usamos estructura prefijo + tema + contexto
    if random.random() < 0.7:
        titulo = f"{random.choice(prefijos)} {random.choice(temas)} {random.choice(contextos)}"
    # Otras veces usamos un formato de pregunta
    else:
        preguntas = [
            f"¿Por qué {random.choice(temas)} es clave {random.choice(contextos)}?",
            f"¿Cómo está transformando {random.choice(temas)} el sector energético?",
            f"¿Cuáles son los avances más prometedores en {random.choice(temas)}?",
            f"¿Qué papel juega {random.choice(temas)} en la transición energética?"
        ]
        titulo = random.choice(preguntas)
    
    # Limpiamos el título de espacios extra y aseguramos que termine correctamente
    titulo = titulo.strip()
    if titulo.endswith(" "):
        titulo = titulo[:-1]
    
    return titulo

@admin_bp.route('/articulos/generar_ia')
@login_required
@admin_required
def generate_ai_article():
    """Genera un borrador usando 3 fuentes y estructura SEO."""
    
    logger.debug('=== Creando artículo con IA ===')
        
    sources = _fetch_articles()
    if not sources:
        flash('No se encontraron noticias relevantes.', 'warning')
        return redirect(url_for('admin.manage_articles'))

    # Elegir título base (primera fuente)
    base = sources[0]
    title = base['title'] if base['title'] else 'Artículo sobre eficiencia energética'
    description = base.get('description', '')
    base_tema = base.get('tema', 'energía')

    # Limitar referencias a la misma temática del artículo base para evitar mezclar tópicos
    related_sources = [base] + [src for src in sources[1:] if src.get('tema') == base_tema]
    # Aviso si no hay fuentes relacionadas adicionales (fallback a solo la base)
    if len(related_sources) == 1:
        logger.warning("No se han encontrado fuentes adicionales relacionadas con el tema base; se utilizará solo la fuente base.")

    # Listado de fuentes a incorporar (solo relacionadas con el tema del título)
    bullet_sources = "\n".join([
        f"- {src['title']} (fuente: {src.get('source', 'Prensa especializada')}: {src['url']})" for src in related_sources
    ])

    # Palabra clave principal basada en la fuente base
    keyword_principal = base_tema if base_tema else "eficiencia energética"
    
    # Calcular un hash único basado en las fuentes para evitar duplicación
    source_hash = hashlib.md5(bullet_sources.encode()).hexdigest()[:6]
    timestamp = datetime.utcnow().strftime('%d%H%M')
    unique_seed = f"{source_hash}_{timestamp}"
    
    logger.info(f"Generando artículo sobre: {keyword_principal} con {len(sources)} fuentes")
    
    prompt = (
        f"Actúa como periodista especializado en el sector de la energía en España (estilo *https://elperiodicodelaenergia.com/*), tecnología energética y evita coletillas de IA. ID único: {unique_seed}\n\n"
        f"TÍTULO FIJO (NO LO REESCRIBAS NI LO INCLUYAS EN EL TEXTO): '{title}'.\n"
        f"Tema principal: '{keyword_principal}'.\n"
        f"Contexto de la noticia base: {description}\n\n"
        "OBJETIVO: Redactar EN ESPAÑOL un artículo de blog COMPLETAMENTE ORIGINAL y ESTRICTAMENTE ALINEADO con el título fijo.\n"
        "FORMATO: Escribe la salida en Markdown puro (sin etiquetas HTML). No incluyas el H1 en el contenido. Empieza directamente por la bajada/introducción.\n\n"
        "=== ESTRUCTURA DETALLADA DEL ARTÍCULO (SIN H1) ===\n"
        "1. SUBTÍTULO/BAJADA:\n"
        "   - Una frase de 15-25 palabras\n"
        "   - Incluye un dato impactante y un ángulo sobre por qué importa el tema\n\n"
        "2. LEAD/INTRODUCCIÓN:\n"
        "   - 3-4 líneas que respondan al qué, quién y por qué\n"
        f"   - Usa la palabra clave principal '{keyword_principal}' en el primer párrafo\n\n"
        "3. CUERPO PRINCIPAL (1500-2000 palabras en total):\n"
        "   - Dividir en 3-4 apartados (H2) con ideas claras y TÍTULOS ORIGINALES\n"
        "   - Incluir: contexto/antecedentes, datos clave, citas relevantes, implicaciones/impacto, próximos pasos\n"
        f"   - IMPORTANTE: Enfoca el contenido específicamente en un ángulo único sobre '{keyword_principal}'\n"
        "   - Cada apartado debe estar bien desarrollado, no puede parecer un simple resumen de noticias\n\n"
        "4. ELEMENTOS DE APOYO:\n"
        "   - Destacar 1-2 citas importantes en bloques destacados\n\n"
        "5. CONCLUSIÓN/CIERRE:\n"
        "   - 1-2 párrafos de resumen\n"
        "   - Incluir llamada a la acción suave (comentar, suscribirse)\n\n"
        "6. OPTIMIZACIÓN SEO:\n"
        f"   - Sugerir meta-title (<=60 caracteres) coherente con el título fijo y que incluya '{keyword_principal}'\n"
        "   - Sugerir meta-description (140-155 caracteres)\n\n"
        "=== REFERENCIAS DISPONIBLES (MISMA TEMÁTICA) ===\n"
        f"{bullet_sources}\n\n"
        "RESTRICCIONES E INSTRUCCIONES FINALES:\n"
        "- No mezcles temáticas distintas a la de las referencias y el título fijo.\n"
        "- EVITA frases genéricas; sé específico y sustenta con datos/cifras cuando sea posible (incluye la fuente).\n"
        "- Adapta el contenido al público español.\n"
        "- NO copies literalmente: reescribe con tu propio estilo evitando coletillas de IA.\n"
        "- Extensión total objetivo: 1500-2000 palabras.\n"
    )

    # Verificamos que tenemos la API key de OpenAI
    if not OPENAI_API_KEY:
        flash("No se puede generar el artículo: falta la clave API de OpenAI", "error")
        return redirect(url_for('admin.manage_articles'))
        
    # Generar contenido
    logger.info(f"Generando contenido para: {title} (tema: {keyword_principal})")
    content = _generate_article_body(prompt)
    
    if not content:
        flash("Error al generar el contenido del artículo", "error")
        return redirect(url_for('admin.manage_articles'))
        
    # Limpiar notas de IA al final del contenido
    import re  # Importamos re localmente para asegurar que esté disponible
    
    # Verificar y eliminar notas al final
    if "Nota:" in content:
        content = content.split("Nota:")[0].strip()
    elif "INSTRUCCIONES FINALES:" in content:
        content = content.split("INSTRUCCIONES FINALES:")[0].strip()
    elif "SEO Optimization" in content:
        content = content.split("SEO Optimization")[0].strip()
    elif "SEO OPTIMIZATION" in content:
        content = content.split("SEO OPTIMIZATION")[0].strip()
    elif "OPTIMIZACIÓN SEO" in content:
        content = content.split("OPTIMIZACIÓN SEO")[0].strip()
        
    # Procesar el contenido markdown a HTML
    content_html = _convert_markdown_to_html(content)
    
    # Verificar alineación título-contenido (conteo simple de coincidencias de palabras del título)
    try:
        title_words = [w for w in slugify(title).split('-') if len(w) > 3]
        total_matches = sum((content or '').lower().count(w) for w in title_words)
        logger.info(f"Alineación título-contenido: {total_matches} coincidencias de palabras clave del título en el contenido")
    except Exception as ex:
        logger.debug(f"No se pudo calcular alineación título-contenido: {ex}")
    
    # Extraer un resumen del contenido (primeros 200 caracteres)
    content_clean = content.replace('#', '').strip()
    paragraphs = [p for p in content_clean.split('\n\n') if p.strip()]
    summary_raw = paragraphs[0][:200] + '...' if paragraphs else f"Artículo sobre {title}"
    
    # Asegurar que el resumen es texto plano, sin etiquetas HTML
    import re
    summary = re.sub(r'<[^>]*>', '', summary_raw)
    
    try:
        # Intentar generar una imagen destacada
        logger.info("Generando imagen destacada con Freepik...")
        featured_image = _generate_featured_image(title, content)
        
        if not featured_image:
            logger.warning("No se pudo generar imagen con Freepik, no se asignará ninguna imagen...")
            featured_image = None
        else:
            logger.info(f"Imagen destacada generada correctamente: {featured_image}")
        
        # Normalizar ruta por seguridad en la vista previa
        featured_image = _normalize_rel_upload_path(featured_image)
        logger.info(f"featured_image normalizada (generate_ai_article): {featured_image}")
        
        # Crear el objeto article con los datos generados
        article = {
            'id': '',
            'title': title,
            'slug': slugify(title),
            'content': content_html,  # Usamos la versión HTML procesada
            'summary': summary,
            'featured_image': featured_image,
            'is_published': False
        }
        
        # Imprimir en el log para depuración
        logger.info(f"Título: {article['title']}")
        logger.info(f"Resumen: {article['summary']}")
        logger.info(f"Imagen: {article['featured_image']}")
        logger.info(f"Contenido (primeros 100 caracteres): {article['content'][:100] if article['content'] else 'Sin contenido'}")
        
        logger.info(f"Artículo generado con éxito: {title}. Mostrando formulario de creación.")
        flash(f"Artículo generado con éxito: {title}. Por favor revisa y completa la información antes de guardar.", "success")
        
        # Preparar formulario y setear campo oculto con la imagen generada
        form = ArticleForm()
        form.existing_featured_image.data = featured_image

        # Renderizar directamente el formulario con los datos del artículo
        return render_template('admin/blog/form.html', 
                    title="Crear Artículo", 
                    article=article,
                    form_action=url_for('admin.create_article'),
                    action="Crear",
                    form=form)
        
    except Exception as e:
        logger.error(f"Error al generar artículo: {str(e)}")
        flash(f"Error al generar artículo: {str(e)}", "error")
        return redirect(url_for('admin.manage_articles'))


@admin_bp.route('/articulos/nuevo', methods=['GET', 'POST'])
@login_required
@admin_required
def create_article():
    """Crear un nuevo artículo para el blog (con CSRF)."""
    try:
        form = ArticleForm()
        if form.validate_on_submit():
            logger.info("Procesando solicitud POST para crear artículo (CSRF OK)")
            # Datos del formulario
            title = (form.title.data or '').strip()
            slug = (form.slug.data or '').strip()
            summary = (form.summary.data or '').strip()
            content = (form.content.data or '').strip()
            is_published = bool(form.is_published.data)

            if not slug:
                slug = slugify(title)

            # Imagen destacada
            featured_image = None
            file = form.featured_image.data
            logger.debug(f"existing_featured_image (POST): {form.existing_featured_image.data}")
            if file and getattr(file, 'filename', '') and allowed_file(file.filename):
                filename = secure_filename(file.filename)
                timestamp = int(time.time())
                filename = f"{timestamp}_{filename}"
                upload_folder = os.path.join(current_app.config['UPLOAD_FOLDER'], 'blog')
                os.makedirs(upload_folder, exist_ok=True)
                file_path = os.path.join(upload_folder, filename)
                file.save(file_path)
                featured_image = f"blog/{filename}"
                # Asegurarse de actualizar también existing_featured_image para consistencia
                form.existing_featured_image.data = featured_image
            else:
                # Mantener imagen existente si no se subió nueva imagen
                featured_image = form.existing_featured_image.data or None
                logger.info(f"Usando imagen existente: {featured_image}")

            # Normalizar ruta relativa
            featured_image = _normalize_rel_upload_path(featured_image)
            logger.info(f"featured_image normalizada (create): {featured_image}")

            try:
                content_to_save = _maybe_convert_to_html(content)
                logger.info(f"Intentando crear artículo con título: {title}, slug: {slug}, featured_image: {featured_image}")
                new_article = Article(
                    title=title,
                    slug=slug,
                    summary=summary,
                    content=content_to_save,
                    is_published=is_published,
                    featured_image=featured_image,
                    user_id=current_user.id
                )
                db.session.add(new_article)
                db.session.commit()
                logger.info(f"Artículo guardado exitosamente con ID: {new_article.id}")
                flash(f'Artículo "{title}" creado con éxito.', 'success')
                return redirect(url_for('admin.manage_articles'))
            except Exception as e:
                db.session.rollback()
                logger.error(f"Error al guardar el artículo: {str(e)}")
                flash(f'Error al guardar el artículo: {str(e)}', 'danger')
                article = {
                    'id': '',
                    'title': title,
                    'slug': slug,
                    'summary': summary,
                    'content': content,
                    'is_published': is_published,
                    'featured_image': featured_image  # Asegurarnos de pasar la imagen procesada
                }
                # Asegurar que el hidden field viaje en el reintento
                try:
                    form.existing_featured_image.data = featured_image
                except Exception:
                    pass
                return render_template('admin/blog/form.html',
                                       title="Crear Artículo",
                                       article=article,
                                       action="Crear",
                                       form_action=url_for('admin.create_article'),
                                       form=form)

        # GET o validación fallida
        article = {
            'id': '',
            'title': (request.form.get('title') or ''),
            'slug': (request.form.get('slug') or ''),
            'summary': (request.form.get('summary') or ''),
            'content': (request.form.get('content') or ''),
            'is_published': 'is_published' in request.form,
        }
        
        # Manejar correctamente la imagen destacada existente
        existing_img = request.form.get('existing_featured_image')
        if existing_img:
            article['featured_image'] = _normalize_rel_upload_path(existing_img)
            # Actualizar también el campo del formulario para consistencia
            form.existing_featured_image.data = article['featured_image']
        else:
            article['featured_image'] = None

        return render_template('admin/blog/form.html',
                               title="Crear Nuevo Artículo",
                               article=article,
                               action="Crear",
                               form_action=url_for('admin.create_article'),
                               form=form)
    except Exception as e:
        logger.error(f"Error general en create_article: {str(e)}")
        flash(f'Error al preparar el formulario: {str(e)}', 'danger')
        return redirect(url_for('admin.manage_articles'))


@admin_bp.route('/articulos/<int:article_id>/editar', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_article(article_id):
    """Editar un artículo existente."""
    try:
        # Buscar el artículo por ID en la base de datos
        article = Article.query.get_or_404(article_id)
        
        form = ArticleForm()
        if form.validate_on_submit():
            try:
                # Datos del formulario
                title = (form.title.data or '').strip()
                slug = (form.slug.data or '').strip()
                summary = (form.summary.data or '').strip()
                content = (form.content.data or '').strip()
                is_published = bool(form.is_published.data)
                
                # Validar datos requeridos
                if not title or not content:
                    flash('El título y contenido son obligatorios', 'danger')
                    return render_template('admin/blog/form.html', article=article, form=form)
                    
                # Actualizar slug si está vacío o si ha cambiado el título y el slug no ha sido personalizado
                if not slug or (article.title != title and article.slug == slugify(article.title)):
                    slug = slugify(title)
                
                # Manejar imagen destacada
                old_featured = article.featured_image
                featured_image = old_featured
                
                # Verificar si se ha marcado la opción de eliminar imagen
                if form.remove_image.data:
                    logger.info(f"Eliminando imagen destacada del artículo {article.id}")
                    # Borrar físicamente la imagen previa si existe
                    abs_old = _abs_upload_path(old_featured)
                    if abs_old and os.path.isfile(abs_old):
                        try:
                            os.remove(abs_old)
                            logger.info(f"Imagen física eliminada: {abs_old}")
                        except Exception as ex:
                            logger.warning(f"No se pudo eliminar la imagen física {abs_old}: {ex}")
                    featured_image = None
                # Si no se elimina, verificar si hay una nueva imagen subida    
                elif form.featured_image.data and getattr(form.featured_image.data, 'filename', ''):
                    file = form.featured_image.data
                    if file and allowed_file(file.filename):
                        # Guardar archivo
                        filename = secure_filename(file.filename)
                        timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
                        filename = f"{timestamp}_{filename}"
                        
                        # Asegurar que el directorio existe
                        upload_folder = os.path.join(current_app.config['UPLOAD_FOLDER'], 'blog')
                        os.makedirs(upload_folder, exist_ok=True)
                        
                        # Guardar archivo
                        file_path = os.path.join(upload_folder, filename)
                        file.save(file_path)
                        
                        # Actualizar ruta para la base de datos
                        featured_image = _normalize_rel_upload_path(f"blog/{filename}")
                        logger.info(f"Nueva imagen destacada subida: {featured_image}")
                        # Eliminar la imagen anterior si existía y es distinta
                        if old_featured and _normalize_rel_upload_path(old_featured) != featured_image:
                            abs_old = _abs_upload_path(old_featured)
                            if abs_old and os.path.isfile(abs_old):
                                try:
                                    os.remove(abs_old)
                                    logger.info(f"Imagen anterior eliminada: {abs_old}")
                                except Exception as ex:
                                    logger.warning(f"No se pudo eliminar la imagen anterior {abs_old}: {ex}")
                # Mantener la imagen existente si no se elimina ni se sube una nueva
                elif form.existing_featured_image.data:
                    featured_image = _normalize_rel_upload_path(form.existing_featured_image.data)
                    logger.debug(f"Manteniendo imagen destacada existente: {featured_image}")
                # Si es un artículo nuevo sin imagen, intentar generar una con Freepik
                elif not article.id:
                    logger.info(f"Intentando generar imagen con Freepik para nuevo artículo: {title}")
                    featured_image = _generate_featured_image(title, content)
                    if featured_image:
                        logger.info(f"Imagen generada automáticamente: {featured_image}")
                    else:
                        logger.warning("No se pudo generar imagen automáticamente")
                
                # Normalizar por seguridad
                featured_image = _normalize_rel_upload_path(featured_image)
                logger.info(f"featured_image normalizada (edit): {featured_image}")

                # Actualizar datos del artículo
                article.title = title
                article.slug = slug
                article.summary = summary
                article.content = _maybe_convert_to_html(content)
                article.featured_image = featured_image
                
                # Manejar el estado de publicación
                was_published = article.is_published
                article.is_published = is_published
                
                # Si se publica por primera vez, establecer la fecha de publicación
                if article.is_published and not was_published and hasattr(article, 'published_at'):
                    article.published_at = datetime.utcnow()
                # Si se despublica, quitar la fecha de publicación
                elif not article.is_published and was_published and hasattr(article, 'published_at'):
                    article.published_at = None
                
                # Guardar los cambios en la base de datos
                db.session.commit()
                flash('¡Artículo actualizado con éxito!', 'success')
                return redirect(url_for('admin.manage_articles'))
                
            except Exception as e:
                db.session.rollback()
                logger.error(f"Error al actualizar artículo: {str(e)}")
                flash(f'Error al actualizar el artículo: {str(e)}', 'danger')
        
        # Pre-poblar campo oculto con la imagen actual para que viaje en el POST
        form.existing_featured_image.data = _normalize_rel_upload_path(article.featured_image)

        # Mostrar el formulario con los datos actuales del artículo
        return render_template('admin/blog/form.html', 
                            article=article,
                            title="Editar Artículo", 
                            action="Actualizar",
                            form=form)  
                          
    except Exception as e:
        logger.error(f"Error al editar artículo {article_id}: {str(e)}")
        flash(f'Error al cargar el artículo: {str(e)}', 'danger')
        return redirect(url_for('admin.manage_articles'))


@admin_bp.route('/articulos/<int:article_id>/eliminar', methods=['POST'])
@login_required
@admin_required
def delete_article(article_id):
    """Eliminar un artículo del blog."""
    try:
        # Log de llegada de petición y datos recibidos
        current_app.logger.info(f"[DELETE] Solicitud de eliminación recibida para artículo ID={article_id}")
        current_app.logger.debug(f"[DELETE] request.method={request.method}, form_keys={list(request.form.keys())}")
        current_app.logger.debug(f"[DELETE] csrf_token(form)={request.form.get('csrf_token')}")
        # Validación CSRF mediante formulario vacío
        form = EmptyForm()
        if not form.validate_on_submit():
            current_app.logger.warning("[DELETE] Validación CSRF fallida en delete_article()")
            flash('Token CSRF inválido o ausente. Intente de nuevo.', 'danger')
            return redirect(url_for('admin.manage_articles'))

        # Buscar el artículo por ID en la base de datos (tras validar CSRF)
        article = Article.query.get_or_404(article_id)

        # Guardar el título para usarlo en el mensaje de confirmación
        title = article.title
        
        # Eliminar físicamente la imagen destacada si existe
        if article.featured_image:
            abs_path = _abs_upload_path(article.featured_image)
            if abs_path and os.path.isfile(abs_path):
                try:
                    os.remove(abs_path)
                    current_app.logger.info(f"[DELETE] Imagen destacada eliminada junto con artículo: {abs_path}")
                except Exception as ex:
                    current_app.logger.warning(f"[DELETE] No se pudo eliminar la imagen {abs_path}: {ex}")

        # Eliminar el artículo de la base de datos
        db.session.delete(article)
        db.session.commit()
        current_app.logger.info(f"[DELETE] Artículo ID={article_id} eliminado de la base de datos")
        
        flash(f'Artículo "{title}" eliminado correctamente', 'success')
    except Exception as e:
        db.session.rollback()
        current_app.logger.exception(f"[DELETE] Error al eliminar artículo ID={article_id}: {e}")
        flash(f'Error al eliminar el artículo: {str(e)}', 'danger')
    
    return redirect(url_for('admin.manage_articles'))


@admin_bp.route('/articulos/<int:article_id>/vista-previa')
@login_required
@admin_required
def preview_article(article_id):
    """Vista previa de un artículo específico."""
    try:
        # Buscar el artículo por ID en la base de datos
        article = Article.query.filter_by(id=article_id).first_or_404()
        
        # Usar el método to_dict() para convertir a diccionario de forma consistente
        article_dict = article.to_dict()
        # Normalizar la ruta de la imagen por seguridad (por si en BD hubiese rutas absolutas antiguas)
        article_dict['featured_image'] = _normalize_rel_upload_path(article_dict.get('featured_image'))
        
        # Añadir logging detallado para depurar la ruta de la imagen
        logger.info(f"Preview article: {article.id} - {article.title}")
        logger.info(f"Featured image path (normalized): {article_dict.get('featured_image')}")
        logger.info(f"Full article dict: {article_dict}")
        
        return render_template('admin/blog/preview.html', 
                            title=f"Vista previa: {article.title}",
                            article=article_dict)
    except Exception as e:
        flash(f"Error al cargar la vista previa: {str(e)}", 'danger')
        return redirect(url_for('admin.manage_articles'))

