"""Weekly blog automation: plan, background generation, scheduled publish."""

import logging
from datetime import date

from django.utils import timezone

from blogs.models import BlogArticle, BlogSettings
from blogs.services.background_jobs import enqueue_process_and_publish_article
from blogs.services.blog_research_service import refresh_external_blog_inventory
from blogs.services.content_service import generate_article_content
from blogs.services.editorial_service import generate_weekly_topics
from blogs.services.image_service import BlogImageService
from blogs.services.scheduling_service import random_publish_dates, week_bounds
from blogs.services.shopify_blog_service import ShopifyBlogService
from blogs.services.trends_service import refresh_trending_topics

logger = logging.getLogger(__name__)


class BlogAutomationService:
    @staticmethod
    def _published_this_week_count(reference=None):
        start, end = week_bounds(reference)
        return BlogArticle.objects.filter(
            status=BlogArticle.STATUS_PUBLISHED,
            posted_at__date__gte=start,
            posted_at__date__lte=end,
        ).count()

    @staticmethod
    def _scheduled_this_week_count(reference=None):
        start, end = week_bounds(reference)
        return (
            BlogArticle.objects.filter(
                scheduled_date__gte=start,
                scheduled_date__lte=end,
            )
            .exclude(status=BlogArticle.STATUS_FAILED)
            .count()
        )

    @staticmethod
    def _needs_generation(article):
        return not (article.content or "").strip() or article.word_count is None

    @classmethod
    def _prepare_planning_data(cls, settings_obj: BlogSettings) -> dict:
        """Track A + B: research live blogs and refresh trending topics."""
        prep = {"external_count": 0, "trending_count": 0, "warnings": []}

        if (settings_obj.blog_list_url or "").strip():
            try:
                inv = refresh_external_blog_inventory(settings_obj)
                prep["external_count"] = len(inv)
            except Exception as exc:
                logger.exception("external blog research failed")
                prep["warnings"].append(f"blog research: {exc}")
        else:
            settings_obj.external_blog_inventory_error = "blog_list_url not set"
            settings_obj.save(
                update_fields=["external_blog_inventory_error", "updated_at"]
            )
            prep["warnings"].append("blog_list_url not set; skipped live blog research")

        settings_obj.refresh_from_db()
        refresh_trending_topics(settings_obj)
        settings_obj.refresh_from_db()
        prep["trending_count"] = len(settings_obj.trending_topics or [])
        return prep

    @classmethod
    def plan_weekly_posts(cls, count, reference=None, dry_run=False):
        """
        A → B → C: research live blogs, refresh trends, plan articles.
        """
        settings_obj = BlogSettings.load()
        prep = cls._prepare_planning_data(settings_obj)
        settings_obj.refresh_from_db()

        today = reference or date.today()
        cw = today.isocalendar()[1]
        publish_dates = random_publish_dates(count, today)

        if dry_run:
            preview = generate_weekly_topics(
                count,
                calendar_week=cw,
                scheduled_dates=publish_dates,
                dry_run=True,
            )
            return {
                "dry_run": True,
                "preview_topics": preview,
                "prep": prep,
            }

        articles = generate_weekly_topics(
            count,
            calendar_week=cw,
            scheduled_dates=publish_dates,
        )
        return articles

    @classmethod
    def generate_and_publish_article(cls, reference=None, background=False):
        """
        Full pipeline: research blog URL, refresh trending topics, plan one article,
        then generate content and publish to Shopify.
        """
        settings_obj = BlogSettings.load()

        if not (settings_obj.shopify_blog_id or "").strip():
            raise ValueError("shopify_blog_id required in Blog settings")
        if not (settings_obj.description or "").strip():
            raise ValueError("description required in Blog settings")

        prep = cls._prepare_planning_data(settings_obj)
        settings_obj.refresh_from_db()

        today = reference or date.today()
        cw = today.isocalendar()[1]
        articles = generate_weekly_topics(
            1,
            calendar_week=cw,
            scheduled_dates=[today],
        )
        if not articles:
            raise ValueError("Planning produced no article")

        article = articles[0]
        result = {
            "ok": True,
            "article_id": article.pk,
            "title": article.title,
            "prep": prep,
        }

        try:
            if background:
                pid = enqueue_process_and_publish_article(article.pk)
                result["background"] = True
                result["worker_pid"] = pid
                result["log_path"] = f"logs/blog_article_{article.pk}.log"
            else:
                cls.process_and_publish_article(article.pk)
                article.refresh_from_db()
                result["shopify_article_id"] = article.shopify_article_id
                result["shopify_article_url"] = article.shopify_article_url
                result["status"] = article.status

            settings_obj.last_run_at = timezone.now()
            settings_obj.last_run_status = "success"
            settings_obj.last_run_error = ""
            settings_obj.save(
                update_fields=["last_run_at", "last_run_status", "last_run_error"]
            )
            return result
        except Exception as exc:
            settings_obj.last_run_at = timezone.now()
            settings_obj.last_run_status = "failed"
            settings_obj.last_run_error = str(exc)
            settings_obj.save(
                update_fields=["last_run_at", "last_run_status", "last_run_error"]
            )
            raise

    @classmethod
    def process_article(cls, article_id):
        """Generate content + images and mark ready (blocking — for workers)."""
        settings_obj = BlogSettings.load()
        article = BlogArticle.objects.get(pk=article_id)
        if article.status == BlogArticle.STATUS_PUBLISHED:
            return article

        generate_article_content(article_id)
        if settings_obj.include_images:
            BlogImageService.generate_for_article(article_id)

        article.refresh_from_db()
        article.status = BlogArticle.STATUS_READY
        article.generation_error = ""
        article.save(update_fields=["status", "generation_error", "updated_at"])
        return article

    @classmethod
    def process_and_publish_article(cls, article_id):
        """Generate content/images and publish to Shopify (blocking worker)."""
        article = cls.process_article(article_id)
        if article.status == BlogArticle.STATUS_PUBLISHED:
            return article
        ShopifyBlogService.publish_article(article_id, published=True)
        return BlogArticle.objects.get(pk=article_id)

    @classmethod
    def publish_due_articles(cls, reference=None, sync=False):
        """
        On each article's scheduled_date: generate + publish (background by default).
        Already-ready posts are published immediately without regenerating.
        """
        today = reference or date.today()
        due = list(
            BlogArticle.objects.filter(
                scheduled_date__lte=today,
                status__in=(
                    BlogArticle.STATUS_PLANNED,
                    BlogArticle.STATUS_READY,
                    BlogArticle.STATUS_FAILED,
                ),
            ).order_by("scheduled_date", "id")
        )

        results = {
            "published": 0,
            "enqueued": [],
            "failed": 0,
            "errors": [],
            "article_ids": [],
        }

        for article in due:
            if article.status == BlogArticle.STATUS_READY:
                try:
                    ShopifyBlogService.publish_article(article.id, published=True)
                    results["published"] += 1
                    results["article_ids"].append(article.id)
                except Exception as exc:
                    logger.exception("publish ready failed article=%s", article.id)
                    results["failed"] += 1
                    results["errors"].append(
                        {"article_id": article.id, "error": str(exc)}
                    )
                continue

            if article.status == BlogArticle.STATUS_GENERATING:
                continue

            if not cls._needs_generation(article) and article.content:
                try:
                    article.status = BlogArticle.STATUS_READY
                    article.save(update_fields=["status", "updated_at"])
                    ShopifyBlogService.publish_article(article.id, published=True)
                    results["published"] += 1
                    results["article_ids"].append(article.id)
                except Exception as exc:
                    results["failed"] += 1
                    results["errors"].append(
                        {"article_id": article.id, "error": str(exc)}
                    )
                continue

            if sync:
                try:
                    cls.process_and_publish_article(article.id)
                    results["published"] += 1
                    results["article_ids"].append(article.id)
                except Exception as exc:
                    logger.exception(
                        "process+publish failed article=%s", article.id
                    )
                    results["failed"] += 1
                    results["errors"].append(
                        {"article_id": article.id, "error": str(exc)}
                    )
            else:
                pid = enqueue_process_and_publish_article(article.id)
                results["enqueued"].append(article.id)
                results.setdefault("worker_pids", []).append(pid)

        results["ok"] = results["failed"] == 0
        return results

    @classmethod
    def run_weekly(cls, limit=None, dry_run=False, sync=False):
        """
        Plan posts for the week: research live blogs, refresh trends, plan topics.
        Generation + publish run on each scheduled_date via publish_due_blog_articles.
        """
        settings_obj = BlogSettings.load()
        if not settings_obj.enabled and not limit:
            return {"ok": False, "error": "Blog automation disabled in settings"}

        if not settings_obj.shopify_blog_id:
            return {"ok": False, "error": "shopify_blog_id required in Blog settings"}

        if not (settings_obj.description or "").strip():
            return {"ok": False, "error": "description required in Blog settings"}

        target = limit or settings_obj.posts_per_week
        published = cls._published_this_week_count()
        scheduled = cls._scheduled_this_week_count()
        slots_left = max(0, target - scheduled)

        to_plan = slots_left if not limit else min(limit, slots_left or limit)
        if limit and sync:
            to_plan = limit

        results = {
            "planned": 0,
            "enqueued": [],
            "published": 0,
            "failed": 0,
            "errors": [],
        }

        try:
            if to_plan > 0:
                plan_result = cls.plan_weekly_posts(to_plan, dry_run=dry_run)
                if dry_run:
                    results["dry_run"] = True
                    results["would_plan"] = to_plan
                    results["preview_topics"] = plan_result.get("preview_topics", [])
                    results["prep"] = plan_result.get("prep", {})
                    results["ok"] = True
                    return results

                new_articles = plan_result
                results["planned"] = len(new_articles)
                results["scheduled_dates"] = [
                    str(a.scheduled_date) for a in new_articles if a.scheduled_date
                ]
            else:
                new_articles = []

            if not dry_run and not sync and to_plan == 0:
                settings_obj.last_run_at = timezone.now()
                settings_obj.last_run_status = "skipped"
                settings_obj.last_run_error = "Weekly schedule already full"
                settings_obj.save(
                    update_fields=["last_run_at", "last_run_status", "last_run_error"]
                )
                return {
                    "ok": True,
                    "skipped": True,
                    "published_this_week": published,
                    "scheduled_this_week": scheduled,
                }

            if sync and new_articles:
                for article in new_articles[: (limit or len(new_articles))]:
                    try:
                        cls.process_and_publish_article(article.id)
                        results["published"] += 1
                    except Exception as exc:
                        results["failed"] += 1
                        results["errors"].append(
                            {"article_id": article.id, "error": str(exc)}
                        )

            settings_obj.last_run_at = timezone.now()
            settings_obj.last_run_status = (
                "success" if not results["failed"] else "partial"
            )
            settings_obj.last_run_error = (
                str(results["errors"][:3]) if results["errors"] else ""
            )
            settings_obj.save(
                update_fields=["last_run_at", "last_run_status", "last_run_error"]
            )
            results["ok"] = True
            results["published_this_week"] = published
            results["scheduled_this_week"] = cls._scheduled_this_week_count()
            return results
        except Exception as exc:
            settings_obj.last_run_at = timezone.now()
            settings_obj.last_run_status = "failed"
            settings_obj.last_run_error = str(exc)
            settings_obj.save(
                update_fields=["last_run_at", "last_run_status", "last_run_error"]
            )
            raise
