"""
Public routes for StarPunk

Handles public-facing pages including homepage and note permalinks.
No authentication required for these routes.
"""

import hashlib
from datetime import datetime, timedelta

from flask import Blueprint, abort, render_template, Response, current_app, request

from starpunk.notes import list_notes, get_note
from starpunk.feed import generate_feed_streaming  # Legacy RSS
from starpunk.feeds import (
    generate_rss,
    generate_rss_streaming,
    generate_atom,
    generate_atom_streaming,
    generate_json_feed,
    generate_json_feed_streaming,
    negotiate_feed_format,
    get_mime_type,
    get_cache,
    generate_opml,
)

# Create blueprint
bp = Blueprint("public", __name__)

# Simple in-memory cache for feed note list
# Caches the database query results to avoid repeated DB hits
# Feed content is now cached via FeedCache (Phase 3)
# Structure: {'notes': list[Note], 'timestamp': datetime}
_feed_cache = {"notes": None, "timestamp": None}


def _get_cached_notes():
    """
    Get cached note list or fetch fresh notes

    Returns cached notes if still valid, otherwise fetches fresh notes
    from database and updates cache.

    Returns:
        List of published notes for feed generation
    """
    # Get cache duration from config (in seconds)
    cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)
    cache_duration = timedelta(seconds=cache_seconds)
    now = datetime.utcnow()

    # Check if note list cache is valid
    if _feed_cache["notes"] and _feed_cache["timestamp"]:
        cache_age = now - _feed_cache["timestamp"]
        if cache_age < cache_duration:
            # Use cached note list
            return _feed_cache["notes"]

    # Cache expired or empty, fetch fresh notes
    max_items = current_app.config.get("FEED_MAX_ITEMS", 50)
    notes = list_notes(published_only=True, limit=max_items)
    _feed_cache["notes"] = notes
    _feed_cache["timestamp"] = now

    return notes


def _generate_feed_with_cache(format_name: str, non_streaming_generator):
    """
    Generate feed with caching and ETag support.

    Implements Phase 3 feed caching:
    - Checks If-None-Match header for conditional requests
    - Uses FeedCache for content caching
    - Returns 304 Not Modified when appropriate
    - Adds ETag header to all responses

    Args:
        format_name: Feed format (rss, atom, json)
        non_streaming_generator: Function that returns full feed content (not streaming)

    Returns:
        Flask Response with appropriate headers and status
    """
    # Get cached notes
    notes = _get_cached_notes()

    # Check if caching is enabled
    cache_enabled = current_app.config.get("FEED_CACHE_ENABLED", True)

    if not cache_enabled:
        # Caching disabled, generate fresh feed
        max_items = current_app.config.get("FEED_MAX_ITEMS", 50)
        cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)

        # Generate feed content (non-streaming)
        content = non_streaming_generator(
            site_url=current_app.config["SITE_URL"],
            site_name=current_app.config["SITE_NAME"],
            site_description=current_app.config.get("SITE_DESCRIPTION", ""),
            notes=notes,
            limit=max_items,
        )

        response = Response(content, mimetype=get_mime_type(format_name))
        response.headers["Cache-Control"] = f"public, max-age={cache_seconds}"
        return response

    # Caching enabled - use FeedCache
    feed_cache = get_cache()
    notes_checksum = feed_cache.generate_notes_checksum(notes)

    # Check If-None-Match header for conditional requests
    if_none_match = request.headers.get('If-None-Match')

    # Try to get cached feed
    cached_result = feed_cache.get(format_name, notes_checksum)

    if cached_result:
        content, etag = cached_result

        # Check if client has current version
        if if_none_match and if_none_match == etag:
            # Client has current version, return 304 Not Modified
            response = Response(status=304)
            response.headers["ETag"] = etag
            return response

        # Return cached content with ETag
        response = Response(content, mimetype=get_mime_type(format_name))
        response.headers["ETag"] = etag
        cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)
        response.headers["Cache-Control"] = f"public, max-age={cache_seconds}"
        return response

    # Cache miss - generate fresh feed
    max_items = current_app.config.get("FEED_MAX_ITEMS", 50)

    # Generate feed content (non-streaming)
    content = non_streaming_generator(
        site_url=current_app.config["SITE_URL"],
        site_name=current_app.config["SITE_NAME"],
        site_description=current_app.config.get("SITE_DESCRIPTION", ""),
        notes=notes,
        limit=max_items,
    )

    # Store in cache and get ETag
    etag = feed_cache.set(format_name, content, notes_checksum)

    # Return fresh content with ETag
    response = Response(content, mimetype=get_mime_type(format_name))
    response.headers["ETag"] = etag
    cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)
    response.headers["Cache-Control"] = f"public, max-age={cache_seconds}"

    return response


@bp.route("/")
def index():
    """
    Homepage displaying recent published notes

    Returns:
        Rendered homepage template with note list

    Template: templates/index.html
    Microformats: h-feed containing h-entry items
    """
    # Get recent published notes (limit 20)
    notes = list_notes(published_only=True, limit=20)

    return render_template("index.html", notes=notes)


@bp.route("/note/<slug>")
def note(slug: str):
    """
    Individual note permalink page

    Args:
        slug: URL-safe note identifier

    Returns:
        Rendered note template with full content

    Raises:
        404: If note not found or not published

    Template: templates/note.html
    Microformats: h-entry
    """
    # Get note by slug
    note_obj = get_note(slug=slug)

    # Return 404 if note doesn't exist or isn't published
    if not note_obj or not note_obj.published:
        abort(404)

    return render_template("note.html", note=note_obj)


@bp.route("/feed")
def feed():
    """
    Content negotiation endpoint for feeds

    Serves feed in format based on HTTP Accept header:
    - application/rss+xml → RSS 2.0
    - application/atom+xml → ATOM 1.0
    - application/feed+json or application/json → JSON Feed 1.1
    - */* → RSS 2.0 (default)

    If no acceptable format is available, returns 406 Not Acceptable with
    X-Available-Formats header listing supported formats.

    Returns:
        Streaming feed response in negotiated format, or 406 error

    Headers:
        Content-Type: Varies by format
        Cache-Control: public, max-age={FEED_CACHE_SECONDS}
        X-Available-Formats: List of supported formats (on 406 error only)

    Examples:
        >>> # Request with Accept: application/atom+xml
        >>> response = client.get('/feed', headers={'Accept': 'application/atom+xml'})
        >>> response.headers['Content-Type']
        'application/atom+xml; charset=utf-8'

        >>> # Request with no Accept header (defaults to RSS)
        >>> response = client.get('/feed')
        >>> response.headers['Content-Type']
        'application/rss+xml; charset=utf-8'
    """
    # Get Accept header
    accept = request.headers.get('Accept', '*/*')

    # Negotiate format
    available_formats = ['rss', 'atom', 'json']
    try:
        format_name = negotiate_feed_format(accept, available_formats)
    except ValueError:
        # No acceptable format - return 406
        return (
            "Not Acceptable. Supported formats: application/rss+xml, application/atom+xml, application/feed+json",
            406,
            {
                'Content-Type': 'text/plain; charset=utf-8',
                'X-Available-Formats': 'application/rss+xml, application/atom+xml, application/feed+json',
            }
        )

    # Route to appropriate generator
    if format_name == 'rss':
        return feed_rss()
    elif format_name == 'atom':
        return feed_atom()
    elif format_name == 'json':
        return feed_json()
    else:
        # Shouldn't reach here, but be defensive
        return feed_rss()


@bp.route("/feed.rss")
def feed_rss():
    """
    Explicit RSS 2.0 feed endpoint (with caching)

    Generates standards-compliant RSS 2.0 feed with Phase 3 caching:
    - LRU cache with TTL (default 5 minutes)
    - ETag support for conditional requests
    - 304 Not Modified responses
    - SHA-256 checksums

    Returns:
        Cached or fresh RSS 2.0 feed response

    Headers:
        Content-Type: application/rss+xml; charset=utf-8
        Cache-Control: public, max-age={FEED_CACHE_SECONDS}
        ETag: W/"sha256_hash"

    Caching Strategy:
        - Database query cached (note list)
        - Feed content cached (full XML)
        - Conditional requests (If-None-Match)
        - Cache invalidation on content changes

    Examples:
        >>> response = client.get('/feed.rss')
        >>> response.status_code
        200
        >>> response.headers['Content-Type']
        'application/rss+xml; charset=utf-8'
        >>> response.headers['ETag']
        'W/"abc123..."'

        >>> # Conditional request
        >>> response = client.get('/feed.rss', headers={'If-None-Match': 'W/"abc123..."'})
        >>> response.status_code
        304
    """
    return _generate_feed_with_cache('rss', generate_rss)


@bp.route("/feed.atom")
def feed_atom():
    """
    Explicit ATOM 1.0 feed endpoint (with caching)

    Generates standards-compliant ATOM 1.0 feed with Phase 3 caching.
    Follows RFC 4287 specification for ATOM syndication format.

    Returns:
        Cached or fresh ATOM 1.0 feed response

    Headers:
        Content-Type: application/atom+xml; charset=utf-8
        Cache-Control: public, max-age={FEED_CACHE_SECONDS}
        ETag: W/"sha256_hash"

    Examples:
        >>> response = client.get('/feed.atom')
        >>> response.status_code
        200
        >>> response.headers['Content-Type']
        'application/atom+xml; charset=utf-8'
        >>> response.headers['ETag']
        'W/"abc123..."'
    """
    return _generate_feed_with_cache('atom', generate_atom)


@bp.route("/feed.json")
def feed_json():
    """
    Explicit JSON Feed 1.1 endpoint (with caching)

    Generates standards-compliant JSON Feed 1.1 feed with Phase 3 caching.
    Follows JSON Feed specification (https://jsonfeed.org/version/1.1).

    Returns:
        Cached or fresh JSON Feed 1.1 response

    Headers:
        Content-Type: application/feed+json; charset=utf-8
        Cache-Control: public, max-age={FEED_CACHE_SECONDS}
        ETag: W/"sha256_hash"

    Examples:
        >>> response = client.get('/feed.json')
        >>> response.status_code
        200
        >>> response.headers['Content-Type']
        'application/feed+json; charset=utf-8'
        >>> response.headers['ETag']
        'W/"abc123..."'
    """
    return _generate_feed_with_cache('json', generate_json_feed)


@bp.route("/feed.xml")
def feed_xml_legacy():
    """
    Legacy RSS 2.0 feed endpoint (backward compatibility)

    Maintains backward compatibility for /feed.xml endpoint.
    New code should use /feed.rss or /feed with content negotiation.

    Returns:
        Streaming RSS 2.0 feed response

    See feed_rss() for full documentation.
    """
    # Use the new RSS endpoint
    return feed_rss()


@bp.route("/opml.xml")
def opml():
    """
    OPML 2.0 feed subscription list endpoint (Phase 3)

    Generates OPML 2.0 document listing all available feed formats.
    Feed readers can import this file to subscribe to all feeds at once.

    Per v1.1.2 Phase 3:
    - OPML 2.0 compliant
    - Lists RSS, ATOM, and JSON Feed formats
    - Public access (no authentication required per CQ8)
    - Enables easy multi-feed subscription

    Returns:
        OPML 2.0 XML document

    Headers:
        Content-Type: application/xml; charset=utf-8
        Cache-Control: public, max-age={FEED_CACHE_SECONDS}

    Examples:
        >>> response = client.get('/opml.xml')
        >>> response.status_code
        200
        >>> response.headers['Content-Type']
        'application/xml; charset=utf-8'
        >>> b'<opml version="2.0">' in response.data
        True

    Standards:
        - OPML 2.0: http://opml.org/spec2.opml
    """
    # Generate OPML content
    opml_content = generate_opml(
        site_url=current_app.config["SITE_URL"],
        site_name=current_app.config["SITE_NAME"],
    )

    # Create response
    response = Response(opml_content, mimetype="application/xml")

    # Add cache headers (same as feed cache duration)
    cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)
    response.headers["Cache-Control"] = f"public, max-age={cache_seconds}"

    return response
