"""Post insights scraping and aggregation service."""
from __future__ import annotations

from collections import defaultdict
from datetime import datetime, date, timezone, timedelta
from typing import Any, Dict, List, Optional, Tuple
from uuid import UUID

from loguru import logger

from src.database.models import LinkedInPostInsightPost, LinkedInPostInsightDaily
from src.scraper import scraper


def _parse_post_date(raw_post: Dict[str, Any]) -> Optional[datetime]:
    posted_at = raw_post.get("posted_at")
    if isinstance(posted_at, dict):
        date_str = posted_at.get("date")
        ts = posted_at.get("timestamp")
        if date_str:
            for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
                try:
                    return datetime.strptime(date_str, fmt)
                except ValueError:
                    continue
            try:
                return datetime.fromisoformat(date_str)
            except ValueError:
                return None
        if ts:
            try:
                return datetime.fromtimestamp(int(ts) / 1000, tz=timezone.utc)
            except Exception:
                return None
    return None


def _extract_post_urn(raw_post: Dict[str, Any]) -> Optional[str]:
    if raw_post.get("full_urn"):
        return raw_post["full_urn"]
    urn_obj = raw_post.get("urn") or {}
    for key, prefix in (("activity_urn", "urn:li:activity:"), ("share_urn", "urn:li:share:"), ("ugcPost_urn", "urn:li:ugcPost:")):
        val = urn_obj.get(key)
        if val:
            if str(val).startswith("urn:li:"):
                return str(val)
            return f"{prefix}{val}"
    return None


def _extract_reactions(stats: Dict[str, Any]) -> Tuple[int, Dict[str, int]]:
    if not stats:
        return 0, {}
    breakdown = {
        "like": int(stats.get("like", 0) or 0),
        "love": int(stats.get("love", 0) or 0),
        "insight": int(stats.get("insight", 0) or 0),
        "celebrate": int(stats.get("celebrate", 0) or 0),
        "support": int(stats.get("support", 0) or 0),
        "funny": int(stats.get("funny", 0) or 0),
    }
    total = stats.get("total_reactions")
    if total is None:
        total = sum(breakdown.values())
    return int(total or 0), breakdown


def _extract_username_from_url(linkedin_url: Optional[str]) -> Optional[str]:
    if not linkedin_url:
        return None
    import re
    url = linkedin_url.rstrip("/")
    match = re.search(r"/in/([^/?#]+)", url)
    if match:
        return match.group(1)
    return None


def parse_apify_posts_for_insights(
    raw_posts: List[Dict[str, Any]],
    user_id: str,
    linkedin_account_id: Optional[str]
) -> Tuple[List[LinkedInPostInsightPost], List[Dict[str, Any]]]:
    """Parse Apify posts into insights models plus raw metadata for snapshots."""
    parsed: List[LinkedInPostInsightPost] = []
    snapshot_meta: List[Dict[str, Any]] = []

    for post in raw_posts:
        post_urn = _extract_post_urn(post)
        if not post_urn:
            continue

        stats = post.get("stats") or {}
        total_reactions, breakdown = _extract_reactions(stats)

        parsed_post = LinkedInPostInsightPost(
            user_id=user_id,
            linkedin_account_id=linkedin_account_id,
            post_urn=post_urn,
            post_url=post.get("url"),
            post_text=post.get("text"),
            post_date=_parse_post_date(post),
            author_username=(post.get("author") or {}).get("username"),
            total_reactions=total_reactions,
            likes=int(stats.get("like", 0) or 0),
            comments=int(stats.get("comments", 0) or 0),
            shares=int(stats.get("reposts", 0) or 0),
            reactions_breakdown=breakdown,
            raw_data=post,
        )
        parsed.append(parsed_post)
        snapshot_meta.append(
            {
                "post_urn": post_urn,
                "total_reactions": total_reactions,
                "likes": int(stats.get("like", 0) or 0),
                "comments": int(stats.get("comments", 0) or 0),
                "shares": int(stats.get("reposts", 0) or 0),
                "reactions_breakdown": breakdown,
            }
        )

    return parsed, snapshot_meta


async def refresh_post_insights_for_account(db, account) -> None:
    """Scrape posts for a connected LinkedIn account and store insights snapshots."""
    vanity_name = None
    try:
        profile = await db.get_profile(account.user_id)
        vanity_name = _extract_username_from_url(profile.linkedin_url if profile else None)
    except Exception:
        vanity_name = None
    if not vanity_name:
        logger.warning(f"LinkedIn account {account.id} missing profile link username; skipping insights scrape.")
        return

    linkedin_url = f"https://www.linkedin.com/in/{vanity_name}/"
    logger.info(f"Scraping post insights for user {account.user_id} ({vanity_name})")

    raw_posts = await scraper.scrape_posts(linkedin_url, limit=50)
    if not raw_posts:
        logger.info(f"No posts found for user {account.user_id}")
        return

    posts, snapshot_meta = parse_apify_posts_for_insights(
        raw_posts,
        user_id=str(account.user_id),
        linkedin_account_id=str(account.id),
    )

    if not posts:
        logger.info(f"No parsable posts for user {account.user_id}")
        return

    saved_posts = await db.upsert_post_insights_posts(posts)
    post_id_by_urn = {p.post_urn: p.id for p in saved_posts}

    today = date.today()
    snapshots: List[LinkedInPostInsightDaily] = []
    for meta in snapshot_meta:
        post_id = post_id_by_urn.get(meta["post_urn"])
        if not post_id:
            continue
        snapshots.append(
            LinkedInPostInsightDaily(
                user_id=account.user_id,
                post_id=post_id,
                snapshot_date=today,
                total_reactions=meta["total_reactions"],
                likes=meta["likes"],
                comments=meta["comments"],
                shares=meta["shares"],
                reactions_breakdown=meta["reactions_breakdown"],
            )
        )

    if snapshots:
        await db.upsert_post_insights_daily(snapshots)
        logger.info(f"Saved {len(snapshots)} post insight snapshots for user {account.user_id}")


async def refresh_all_post_insights(db) -> None:
    """Run daily refresh for all active LinkedIn accounts."""
    accounts = await db.list_linkedin_accounts(active_only=True)
    if not accounts:
        return
    for account in accounts:
        try:
            await refresh_post_insights_for_account(db, account)
        except Exception as exc:
            logger.error(f"Post insights refresh failed for {account.id}: {exc}")


def _word_count(text: Optional[str]) -> int:
    if not text:
        return 0
    return len([w for w in text.split() if w.strip()])


def _to_naive(dt: datetime) -> datetime:
    if dt.tzinfo is not None:
        return dt.astimezone(timezone.utc).replace(tzinfo=None)
    return dt


def compute_post_insights(posts: List[LinkedInPostInsightPost], daily: List[LinkedInPostInsightDaily]) -> Dict[str, Any]:
    """Compute aggregated post insights for UI rendering."""
    if not posts:
        return {"has_data": False}

    posts_by_id = {p.id: p for p in posts if p.id}

    latest_by_post: Dict[str, LinkedInPostInsightDaily] = {}
    daily_by_post: Dict[str, List[LinkedInPostInsightDaily]] = defaultdict(list)
    for snap in daily:
        if not snap.post_id:
            continue
        daily_by_post[str(snap.post_id)].append(snap)
        current = latest_by_post.get(str(snap.post_id))
        if not current or snap.snapshot_date > current.snapshot_date:
            latest_by_post[str(snap.post_id)] = snap

    # Aggregate totals using latest snapshot per post
    total_posts = len(posts_by_id)
    total_reactions = 0
    total_likes = 0
    total_comments = 0
    total_shares = 0
    reaction_mix = defaultdict(int)

    for post_id, snap in latest_by_post.items():
        total_reactions += snap.total_reactions or 0
        total_likes += snap.likes or 0
        total_comments += snap.comments or 0
        total_shares += snap.shares or 0
        for k, v in (snap.reactions_breakdown or {}).items():
            reaction_mix[k] += int(v or 0)

    avg_reactions = round(total_reactions / total_posts, 2) if total_posts else 0
    avg_likes = round(total_likes / total_posts, 2) if total_posts else 0
    avg_comments = round(total_comments / total_posts, 2) if total_posts else 0
    avg_shares = round(total_shares / total_posts, 2) if total_posts else 0

    # Daily deltas (last 30 days)
    delta_by_date = defaultdict(lambda: {"reactions": 0, "likes": 0, "comments": 0, "shares": 0})
    for post_id, snaps in daily_by_post.items():
        snaps_sorted = sorted(snaps, key=lambda s: s.snapshot_date)
        prev = None
        for snap in snaps_sorted:
            if prev:
                day = snap.snapshot_date
                delta_by_date[day]["reactions"] += max(0, (snap.total_reactions or 0) - (prev.total_reactions or 0))
                delta_by_date[day]["likes"] += max(0, (snap.likes or 0) - (prev.likes or 0))
                delta_by_date[day]["comments"] += max(0, (snap.comments or 0) - (prev.comments or 0))
                delta_by_date[day]["shares"] += max(0, (snap.shares or 0) - (prev.shares or 0))
            prev = snap

    today = date.today()
    last_7_days = [today - timedelta(days=i) for i in range(6, -1, -1)]
    series = []
    for day in last_7_days:
        delta = delta_by_date.get(day, {"reactions": 0, "likes": 0, "comments": 0, "shares": 0})
        series.append({"date": day, **delta})

    max_reactions = max([d["reactions"] for d in series], default=0) or 1

    last_7 = sum(d["reactions"] for d in series[-7:])
    prev_7 = sum(d["reactions"] for d in series[-14:-7]) if len(series) >= 14 else 0
    trend_pct = None
    if prev_7:
        trend_pct = round(((last_7 - prev_7) / prev_7) * 100, 1)

    # Top posts by engagement score
    top_posts = []
    for post_id, snap in latest_by_post.items():
        post = None
        if post_id:
            try:
                post = posts_by_id.get(UUID(post_id))
            except ValueError:
                post = None
        if not post:
            continue
        score = (snap.likes or 0) + (snap.comments or 0) * 2 + (snap.shares or 0) * 3
        top_posts.append(
            {
                "post_url": post.post_url,
                "post_date": post.post_date,
                "text": post.post_text or "",
                "likes": snap.likes or 0,
                "comments": snap.comments or 0,
                "shares": snap.shares or 0,
                "total_reactions": snap.total_reactions or 0,
                "engagement_score": score,
            }
        )
    top_posts = sorted(top_posts, key=lambda x: x["engagement_score"], reverse=True)[:5]

    # Posting cadence and weekday performance
    weekday_stats = defaultdict(lambda: {"count": 0, "engagement": 0})
    post_dates = []
    for post in posts_by_id.values():
        if not post.post_date:
            continue
        post_dates.append(_to_naive(post.post_date))
        snap = latest_by_post.get(str(post.id))
        score = 0
        if snap:
            score = (snap.likes or 0) + (snap.comments or 0) * 2 + (snap.shares or 0) * 3
        weekday = post.post_date.strftime("%A")
        weekday_stats[weekday]["count"] += 1
        weekday_stats[weekday]["engagement"] += score

    best_weekday = None
    if weekday_stats:
        best_weekday = max(
            weekday_stats.items(),
            key=lambda kv: (kv[1]["engagement"] / kv[1]["count"]) if kv[1]["count"] else 0
        )[0]
    weekday_breakdown = []
    if weekday_stats:
        order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
        for day in order:
            stats = weekday_stats.get(day, {"count": 0, "engagement": 0})
            avg = round(stats["engagement"] / stats["count"], 2) if stats["count"] else 0
            weekday_breakdown.append({"day": day, "count": stats["count"], "avg_engagement": avg})

    cadence_per_week = None
    if post_dates:
        earliest = min(post_dates)
        weeks = max(1, int((datetime.now() - earliest).days / 7))
        cadence_per_week = round(len(post_dates) / weeks, 2)

    # Length buckets
    length_buckets = {
        "0-100": {"count": 0, "avg_engagement": 0},
        "101-200": {"count": 0, "avg_engagement": 0},
        "201-400": {"count": 0, "avg_engagement": 0},
        "400+": {"count": 0, "avg_engagement": 0},
    }
    for post in posts_by_id.values():
        wc = _word_count(post.post_text)
        snap = latest_by_post.get(str(post.id))
        score = 0
        if snap:
            score = (snap.likes or 0) + (snap.comments or 0) * 2 + (snap.shares or 0) * 3
        if wc <= 100:
            bucket = "0-100"
        elif wc <= 200:
            bucket = "101-200"
        elif wc <= 400:
            bucket = "201-400"
        else:
            bucket = "400+"
        length_buckets[bucket]["count"] += 1
        length_buckets[bucket]["avg_engagement"] += score

    for bucket in length_buckets.values():
        if bucket["count"]:
            bucket["avg_engagement"] = round(bucket["avg_engagement"] / bucket["count"], 2)

    latest_snapshot_date = None
    if daily:
        latest_snapshot_date = max(s.snapshot_date for s in daily)

    reaction_mix_total = sum(reaction_mix.values()) or 1
    reaction_mix_pct = [
        {
            "name": k,
            "count": v,
            "pct": round((v / reaction_mix_total) * 100, 1),
        }
        for k, v in sorted(reaction_mix.items(), key=lambda x: x[1], reverse=True)
    ]

    series_chart = [
        {
            "date": item["date"].isoformat(),
            "reactions": item["reactions"],
            "likes": item["likes"],
            "comments": item["comments"],
            "shares": item["shares"],
        }
        for item in series
    ]

    return {
        "has_data": True,
        "total_posts": total_posts,
        "total_reactions": total_reactions,
        "avg_reactions": avg_reactions,
        "avg_likes": avg_likes,
        "avg_comments": avg_comments,
        "avg_shares": avg_shares,
        "last_7_reactions": last_7,
        "prev_7_reactions": prev_7,
        "trend_pct": trend_pct,
        "best_weekday": best_weekday,
        "weekday_breakdown": weekday_breakdown,
        "cadence_per_week": cadence_per_week,
        "reaction_mix": reaction_mix_pct,
        "series": series,
        "series_chart": series_chart,
        "series_max": max_reactions,
        "top_posts": top_posts,
        "length_buckets": length_buckets,
        "latest_snapshot_date": latest_snapshot_date,
    }
