"""Main PyView application for FenLiu."""

import json
import traceback
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import UTC
from datetime import date
from datetime import datetime
from datetime import timedelta
from pathlib import Path
from typing import Any

import pyview.pyview
from pyview import PyView
from pyview.template import RootTemplateContext
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy import select as sa_select
from sqlalchemy import text
from sqlalchemy.orm import joinedload
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.responses import RedirectResponse
from starlette.responses import Response
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates

from fenliu.api import api_router
from fenliu.config import settings
from fenliu.database import get_db
from fenliu.liveviews import ReviewLiveView

# Get logger from centralized logging module
from fenliu.logging import get_logger
from fenliu.middleware import APIKeyMiddleware
from fenliu.models import AppSetting
from fenliu.models import BlockedHashtag
from fenliu.models import BlockedUser
from fenliu.models import ErrorHistory
from fenliu.models import HashtagStream
from fenliu.models import Post
from fenliu.models import QueueStats
from fenliu.services.api_key import get_api_key
from fenliu.services.blocklist_cache import initialize_cache
from fenliu.services.export_eligibility import check_export_eligibility
from fenliu.services.scheduler import start_scheduler
from fenliu.services.scheduler import stop_scheduler

logger = get_logger(__name__)

# Initialize Jinja2 templates
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")

# PyView bug workaround: Patch liveview_container to handle UnconnectedSocket

_original_liveview_container = pyview.pyview.liveview_container


# Helper functions for datetime conversion and serialization
def make_datetime_aware(dt: datetime | None) -> datetime | None:
    """Convert naive datetime to timezone-aware UTC datetime."""
    logger.debug(f"make_datetime_aware: input={dt}, tzinfo={dt.tzinfo if dt else None}")
    if dt is not None and dt.tzinfo is None:
        result = dt.replace(tzinfo=UTC)
        logger.debug(f"make_datetime_aware: converted to {result}")
        return result
    return dt


def process_post_for_display(post: Post) -> dict[str, Any]:
    """Convert post object to display-ready dictionary with timezone-aware datetimes."""
    logger.debug(f"process_post_for_display: post_id={post.id}, url={post.url}")
    post_dict = {c.name: getattr(post, c.name) for c in post.__table__.columns}
    post_dict["created_at"] = make_datetime_aware(post.created_at)
    post_dict["fetched_at"] = make_datetime_aware(post.fetched_at)
    # Ensure media_attachments is properly serialized (it's JSON in database)
    if post_dict.get("media_attachments") and isinstance(post_dict["media_attachments"], str):
        try:
            post_dict["media_attachments"] = json.loads(post_dict["media_attachments"])
            logger.debug(f"process_post_for_display: parsed media_attachments for post {post.id}")
        except (json.JSONDecodeError, TypeError) as e:
            logger.debug(f"process_post_for_display: failed to parse media_attachments: {e}")
            post_dict["media_attachments"] = []
    # Include stream information if available
    if post.stream:
        logger.debug(f"process_post_for_display: processing stream {post.stream.hashtag}")
        post_dict["stream"] = process_stream_for_display(post.stream)
    return post_dict


def process_stream_for_display(stream: HashtagStream) -> dict[str, Any]:
    """Convert stream object to display-ready dictionary with timezone-aware datetimes."""
    logger.debug(f"process_stream_for_display: hashtag={stream.hashtag}, instance={stream.instance}")
    stream_dict = {c.name: getattr(stream, c.name) for c in stream.__table__.columns}
    stream_dict["last_check"] = make_datetime_aware(stream.last_check)
    stream_dict["created_at"] = make_datetime_aware(stream.created_at)
    stream_dict["updated_at"] = make_datetime_aware(stream.updated_at)
    stream_dict["next_scheduled_fetch"] = make_datetime_aware(stream.next_scheduled_fetch)
    return stream_dict


async def _patched_liveview_container(template: Any, view_lookup: Any, request: Request) -> Any:
    """Patched version of liveview_container that handles UnconnectedSocket."""
    logger.debug(f"_patched_liveview_container: path={request.url.path}")
    try:
        result = await _original_liveview_container(template, view_lookup, request)
        logger.debug(f"_patched_liveview_container: success for {request.url.path}")
        return result
    except ValueError as e:
        logger.debug(f"_patched_liveview_container: No LiveView for {request.url.path} - {e}")
        # No LiveView found for this path - this is expected for regular HTTP routes
        # Let the request continue to regular HTTP routes
        raise
    except Exception as e:
        # Log any other errors
        logger.error(f"Error in patched liveview_container: {e}")
        logger.error(traceback.format_exc())
        raise


# Apply the patch
pyview.pyview.liveview_container = _patched_liveview_container  # type: ignore


@asynccontextmanager
async def lifespan(_app: PyView) -> AsyncIterator[None]:
    """Lifespan context manager for PyView application."""
    logger.info("Starting FenLiu application...")
    logger.debug(f"Database URL: {settings.database_url}")
    logger.debug(f"Debug mode: {settings.debug}")

    # Initialize blocklist cache for fast eligibility checks
    try:
        with get_db() as db:
            initialize_cache(db)
            logger.info("Blocklist cache initialized")
    except Exception as e:
        logger.error(f"Failed to initialize blocklist cache: {e}", exc_info=True)
        raise

    # Start background scheduler for stream fetches
    try:
        await start_scheduler()
    except Exception as e:
        logger.error(f"Failed to start background scheduler: {e}", exc_info=True)
        raise

    logger.info(f"FenLiu {settings.app_version} started successfully")
    yield

    # Shutdown: Stop background scheduler
    logger.debug("lifespan: shutting down background scheduler")
    try:
        await stop_scheduler()
    except Exception as e:
        logger.error(f"Error stopping background scheduler: {e}", exc_info=True)

    logger.debug("lifespan: shutting down FenLiu application")


# Create PyView application
app = PyView(
    lifespan=lifespan,
    debug=settings.debug,
)

# Add API key authentication middleware
app.add_middleware(APIKeyMiddleware)  # type: ignore[arg-type]

# Mount static files directory
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")


def custom_root_template(context: RootTemplateContext) -> str:
    """Return a custom root template with Tailwind CSS and Font Awesome."""
    # Use context title if provided, otherwise use default
    title = context.get("title") or f"{settings.app_name} v{settings.app_version}"
    render_title = title + " | LiveView" if title else "LiveView"

    return f'''
<!DOCTYPE html>
<html lang="en">
    <head>
      <title data-suffix=" | LiveView">{render_title}</title>
      <meta name="csrf-token" content="{context["csrf_token"]}" />
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

      <!-- Tailwind CSS -->
      <script src="https://cdn.tailwindcss.com"></script>

      <!-- Font Awesome -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />

      <!-- Review Page Styles -->
      <link rel="stylesheet" href="/static/styles/review.css" />

      <!-- Favicon -->
      <link rel="icon" href="/static/FenLiu.png" type="image/png" />
      <link rel="apple-touch-icon" href="/static/FenLiu.png" />

      <script defer type="text/javascript" src="/static/assets/app.js"></script>
      {"\n".join(context["additional_head_elements"])}
    </head>
    <body>
      <div
        data-phx-main="true"
        data-phx-session="{context["session"]}"
        data-phx-static=""
        id="phx-{context["id"]}"
        >
        {context["content"]}
      </div>
    </body>
</html>
'''


# Customize root template
app.rootTemplate = custom_root_template


# Regular HTTP routes (workaround for PyView LiveView bug)


async def dashboard(request: Request) -> Response:
    """Render the dashboard page."""
    with get_db() as db:
        # Get statistics
        total_streams: int = db.scalar(select(func.count()).select_from(HashtagStream))
        active_streams: int = db.scalar(select(func.count()).select_from(HashtagStream).where(HashtagStream.active))
        total_posts: int = db.scalar(select(func.count()).select_from(Post))
        processed_posts: int = db.scalar(select(func.count()).select_from(Post).where(Post.processed))

        # Get curated queue posts (approved and in queue)
        stmt = (
            select(Post)
            .options(joinedload(Post.stream))
            .where(Post.approved.is_(True))
            .where(Post.queue_status.isnot(None))
            .order_by(Post.reserved_at.desc(), Post.export_date.desc())
            .limit(20)
        )
        result = db.execute(stmt)
        recent_posts = list(result.scalars().all())

        # Get active streams
        stmt = select(HashtagStream).where(HashtagStream.active).order_by(HashtagStream.hashtag)
        result = db.execute(stmt)
        active_streams_list = list(result.scalars().all())

        # Process posts to make datetimes timezone-aware and serialize
        processed_posts_list = [process_post_for_display(post) for post in recent_posts]

        # Process streams to make datetimes timezone-aware and add post counts
        processed_streams = []
        for stream in active_streams_list:
            stream_dict = process_stream_for_display(stream)
            # Count posts for this stream
            post_count: int = db.scalar(select(func.count()).select_from(Post).where(Post.stream_id == stream.id))
            stream_dict["post_count"] = post_count
            processed_streams.append(stream_dict)

        # Get API key if it exists
        api_key = get_api_key(db)

        context = {
            "total_streams": total_streams,
            "active_streams": active_streams,
            "total_posts": total_posts,
            "processed_posts": processed_posts,
            "curated_posts": processed_posts_list,
            "active_streams_list": processed_streams,
            "app_name": settings.app_name,
            "app_version": settings.app_version,
            "api_key": api_key or "",
        }
        return templates.TemplateResponse(request, "dashboard.html", context)


async def streams_page(request: Request) -> Response:
    """Render the hashtag streams management page."""
    with get_db() as db:
        stmt = select(HashtagStream).order_by(HashtagStream.created_at.desc())
        result = db.execute(stmt)
        streams = list(result.scalars().all())

        # Process streams using helper function
        processed_streams = [process_stream_for_display(stream) for stream in streams]

        # Get API key if it exists
        api_key = get_api_key(db)

        context = {
            "streams": processed_streams,
            "app_name": settings.app_name,
            "app_version": settings.app_version,
            "default_instance": settings.default_instance,
            "api_key": api_key or "",
        }
        return templates.TemplateResponse(request, "hashtag_streams.html", context)


async def stats_page(request: Request) -> Response:
    """Render the statistics page."""
    with get_db() as db:
        # Get basic statistics
        total_streams: int = db.scalar(select(func.count()).select_from(HashtagStream))
        active_streams: int = db.scalar(select(func.count()).select_from(HashtagStream).where(HashtagStream.active))
        total_posts: int = db.scalar(select(func.count()).select_from(Post))
        processed_posts: int = db.scalar(select(func.count()).select_from(Post).where(Post.reviewed))

        # Count posts with errors (active)
        posts_with_errors: int = db.scalar(select(func.count()).select_from(Post).where(Post.queue_status == "error"))

        # Count historical errors from ErrorHistory table
        historical_errors: int = db.scalar(select(func.count()).select_from(ErrorHistory))
        all_time_errors = posts_with_errors + historical_errors

        # Load queue stats for all-time counts
        stats = db.get(QueueStats, 1)

        # Count delivered posts
        delivered_posts: int = db.scalar(select(func.count()).select_from(Post).where(Post.queue_status == "delivered"))
        all_time_delivered = delivered_posts + (stats.total_deleted_delivered if stats else 0)

        # Get most frequent error reason (combining active + historical)
        most_frequent_error: str | None = None
        if all_time_errors > 0:
            # UNION active errors with historical errors, then find most common reason
            error_query = text("""
                SELECT error_reason, COUNT(*) as count FROM (
                    SELECT error_reason FROM posts WHERE queue_status = 'error' AND error_reason IS NOT NULL
                    UNION ALL
                    SELECT error_reason FROM error_history WHERE error_reason IS NOT NULL
                ) combined
                GROUP BY error_reason
                ORDER BY count DESC
                LIMIT 1
            """)
            error_result = db.execute(error_query).first()
            if error_result:
                most_frequent_error = error_result[0]

        # Calculate average spam score across all posts
        average_spam_score: float | None = db.scalar(select(func.avg(Post.spam_score)))

        # Get top hashtags by post count (including all hashtags from posts)
        top_hashtags_query = text("""
            SELECT
                MIN(json_each.value) as hashtag,
                COUNT(*) as post_count,
                MAX(hs.active) as active
            FROM posts, json_each(posts.hashtags)
            LEFT JOIN hashtag_streams hs ON hs.hashtag = LTRIM(json_each.value, '#') COLLATE NOCASE
            GROUP BY json_each.value COLLATE NOCASE
            ORDER BY post_count DESC
            LIMIT 10
        """)
        result = db.execute(top_hashtags_query)
        top_hashtags = []
        for row in result:
            # Strip the # prefix from hashtag if present
            hashtag = str(row[0]).lstrip("#")
            top_hashtags.append({"hashtag": hashtag, "post_count": row[1], "active": row[2]})

        # Generate posts over time data for last 7 days
        posts_over_time_data: list[int] = []
        posts_over_time_labels: list[str] = []

        # Get today's date
        today: date = date.today()

        for i in range(6, -1, -1):  # Last 7 days including today
            day_date = today - timedelta(days=i)
            day_start = datetime.combine(day_date, datetime.min.time()).replace(tzinfo=UTC)
            day_end = datetime.combine(day_date, datetime.max.time()).replace(tzinfo=UTC)

            # Count posts fetched on this day
            day_count: int = db.scalar(
                select(func.count()).select_from(Post).where(Post.fetched_at >= day_start, Post.fetched_at <= day_end)
            )

            posts_over_time_data.append(day_count)
            # Format label as abbreviated day name (e.g., "Mon")
            posts_over_time_labels.append(day_date.strftime("%a"))

        # Generate top hashtags chart data (top 5)
        top_hashtags_chart_data = []
        top_hashtags_chart_labels = []

        for hashtag in top_hashtags[:5]:
            top_hashtags_chart_labels.append(f"#{hashtag['hashtag']}")
            top_hashtags_chart_data.append(hashtag["post_count"])

        context = {
            "total_streams": total_streams,
            "active_streams": active_streams,
            "total_posts": total_posts,
            "processed_posts": processed_posts,
            "average_spam_score": average_spam_score,
            "top_hashtags": top_hashtags,
            "posts_over_time_data": posts_over_time_data,
            "posts_over_time_labels": posts_over_time_labels,
            "top_hashtags_chart_data": top_hashtags_chart_data,
            "top_hashtags_chart_labels": top_hashtags_chart_labels,
            "posts_with_errors": posts_with_errors,
            "all_time_errors": all_time_errors,
            "delivered_posts": delivered_posts,
            "all_time_delivered": all_time_delivered,
            "most_frequent_error": most_frequent_error,
            "app_name": settings.app_name,
            "app_version": settings.app_version,
        }
        return templates.TemplateResponse(request, "stats.html", context)


async def post_details(request: Request) -> Response:
    """Render the post details page."""
    with get_db() as db:
        try:
            post_id: int = int(request.path_params["post_id"])
        except (KeyError, ValueError):
            return JSONResponse({"detail": "Invalid post ID"}, status_code=400)

        stmt = select(Post).options(joinedload(Post.stream)).where(Post.id == post_id)
        result = db.execute(stmt)
        post: Post | None = result.scalar_one_or_none()

        if not post:
            return JSONResponse({"detail": f"Post {post_id} not found"}, status_code=404)

        # Process post using helper function
        post_dict: dict = process_post_for_display(post)

        # Get stream info if available
        if post.stream:
            post_dict["stream"] = process_stream_for_display(post.stream)

        context = {
            "post": post_dict,
            "app_name": settings.app_name,
            "app_version": settings.app_version,
        }
        return templates.TemplateResponse(request, "post_details.html", context)


# Add API endpoints (RESTful)
app.mount("/api", api_router)


async def health_check(_request: Request) -> JSONResponse:
    """Health check endpoint."""
    return JSONResponse(
        {
            "status": "healthy",
            "app": settings.app_name,
            "version": settings.app_version,
        }
    )


async def settings_page(request: Request) -> Response:
    """Render the settings management page."""
    with get_db() as db:
        blocked_users = list(db.execute(sa_select(BlockedUser).order_by(BlockedUser.created_at.desc())).scalars().all())
        blocked_hashtags = list(
            db.execute(sa_select(BlockedHashtag).order_by(BlockedHashtag.created_at.desc())).scalars().all()
        )

        def _bool_setting(key: str) -> bool:
            row = db.execute(sa_select(AppSetting).where(AppSetting.key == key)).scalar_one_or_none()
            return row is not None and row.value.lower() == "true"

        # Get API key if it exists
        api_key = get_api_key(db)

        context = {
            "blocked_users": blocked_users,
            "blocked_hashtags": blocked_hashtags,
            "attachments_only": _bool_setting("attachments_only"),
            "auto_reject_on_fetch": _bool_setting("auto_reject_blocked"),
            "auto_reject_bots": _bool_setting("auto_reject_bots"),
            "app_name": settings.app_name,
            "app_version": settings.app_version,
            "api_key": api_key or "",
        }
        return templates.TemplateResponse(request, "settings.html", context)


async def queue_preview_page(request: Request) -> Response:
    """Render the Curated queue preview page."""
    # Get query parameters for filtering and sorting
    status_filter = request.query_params.get("status", "all").lower()
    sort_by = request.query_params.get("sort", "reviewed_at").lower()
    sort_order = request.query_params.get("order", "desc").lower()

    with get_db() as db:
        # Get API key if it exists
        api_key = get_api_key(db)
        # Get all approved posts
        stmt = sa_select(Post).where(Post.approved.is_(True))
        approved_posts = list(db.execute(stmt).scalars().all())

        # Count by queue status
        pending_count = sum(1 for p in approved_posts if p.queue_status == "pending")
        reserved_count = sum(1 for p in approved_posts if p.queue_status == "reserved")
        delivered_count = sum(1 for p in approved_posts if p.queue_status == "delivered")
        error_count = sum(1 for p in approved_posts if p.queue_status == "error")

        # Load queue stats for all-time counts
        stats = db.get(QueueStats, 1)
        all_time_delivered = delivered_count + (stats.total_deleted_delivered if stats else 0)
        all_time_pending = pending_count + (stats.total_deleted_pending if stats else 0)

        # Prepare post details with eligibility info
        posts_data = []
        for post in approved_posts:
            eligibility = check_export_eligibility(post, db)
            posts_data.append(
                {
                    "post": post,
                    "queue_status": post.queue_status or "unknown",
                    "eligible": eligibility.eligible,
                    "exclusion_reason": eligibility.reason,
                    "error_reason": post.error_reason,
                    "errored_at": post.errored_at,
                    "reserved_at": post.reserved_at,
                }
            )

        # Apply status filter
        if status_filter != "all":
            posts_data = [p for p in posts_data if p["queue_status"] == status_filter]

        # Apply sorting
        reverse = sort_order == "desc"
        if sort_by == "reviewed_at":
            posts_data.sort(key=lambda p: p["post"].reviewed_at or datetime.min, reverse=reverse)
        elif sort_by == "spam_score":
            posts_data.sort(key=lambda p: p["post"].spam_score, reverse=reverse)
        elif sort_by == "author":
            posts_data.sort(
                key=lambda p: (p["post"].author_display_name or p["post"].author_username or "").lower(),
                reverse=reverse,
            )
        elif sort_by == "errored_at":
            posts_data.sort(key=lambda p: p["post"].errored_at or datetime.min, reverse=reverse)
        elif sort_by == "reserved_at":
            posts_data.sort(key=lambda p: p["post"].reserved_at or datetime.min, reverse=reverse)

        context = {
            "pending_count": pending_count,
            "reserved_count": reserved_count,
            "delivered_count": delivered_count,
            "all_time_delivered": all_time_delivered,
            "all_time_pending": all_time_pending,
            "error_count": error_count,
            "posts": posts_data,
            "app_name": settings.app_name,
            "app_version": settings.app_version,
            "current_status_filter": status_filter,
            "current_sort_by": sort_by,
            "current_sort_order": sort_order,
            "api_key": api_key or "",
        }
        return templates.TemplateResponse(request, "queue_preview.html", context)


async def app_info(_request: Request) -> JSONResponse:
    """Application information endpoint."""
    return JSONResponse(
        {
            "app": settings.app_name,
            "version": settings.app_version,
            "description": "Fediverse content stream filtering system",
            "inspiration": "Dujiangyan irrigation system (256 BC)",
        }
    )


# Register HTTP routes
app.add_route("/", dashboard, methods=["GET"])
app.add_route("/streams", streams_page, methods=["GET"])
app.add_live_view("/review", ReviewLiveView)


async def update_settings(request: Request) -> Response:
    """Handle form submission for updating reblog settings."""
    try:
        form_data = await request.form()
        logger.debug(f"Form data received: {dict(form_data)}")
        with get_db() as db:
            # Helper to set boolean settings
            def _set_bool_setting(key: str, value: bool) -> None:
                row = db.execute(sa_select(AppSetting).where(AppSetting.key == key)).scalar_one_or_none()
                if row is None:
                    db.add(AppSetting(key=key, value=str(value).lower()))
                else:
                    row.value = str(value).lower()
                logger.debug(f"Set {key} to {value}")

            # Update settings from form data - checkboxes return "on" when checked
            attachments_only = form_data.get("attachments_only") == "on"
            auto_reject_on_fetch = form_data.get("auto_reject_on_fetch") == "on"
            auto_reject_bots = form_data.get("auto_reject_bots") == "on"
            logger.debug(
                f"Parsed settings: attachments_only={attachments_only}, "
                f"auto_reject_on_fetch={auto_reject_on_fetch}, auto_reject_bots={auto_reject_bots}"
            )

            _set_bool_setting("attachments_only", attachments_only)
            _set_bool_setting("auto_reject_blocked", auto_reject_on_fetch)
            _set_bool_setting("auto_reject_bots", auto_reject_bots)
            db.commit()
            logger.info(
                f"Settings updated: attachments_only={attachments_only}, "
                f"auto_reject_on_fetch={auto_reject_on_fetch}, auto_reject_bots={auto_reject_bots}"
            )

        # Redirect back to settings page
        return RedirectResponse(url="/settings", status_code=303)
    except Exception as e:
        logger.error(f"Error updating settings: {e}", exc_info=True)
        return RedirectResponse(url="/settings", status_code=303)


app.add_route("/stats", stats_page, methods=["GET"])
app.add_route("/posts/{post_id}", post_details, methods=["GET"])
app.add_route("/settings", settings_page, methods=["GET"])
app.add_route("/settings", update_settings, methods=["POST"])
app.add_route("/queue", queue_preview_page, methods=["GET"])
app.add_route("/health", health_check, methods=["GET"])
app.add_route("/info", app_info, methods=["GET"])
