"""Blog routes for AgentForms content marketing.

Markdown-based blog with admin CRUD. Posts stored in
~/.hermes/blogs/agentforms/ directory.

Routes:
    GET /blog/ — List all posts
    GET /blog/<slug> — Single post
    GET /blog/admin/ — Admin post list (login_required)
    GET /blog/admin/new — Create new post
    POST /blog/admin/new — Save new post
    GET /blog/admin/<slug>/edit — Edit existing post
    POST /blog/admin/<slug>/edit — Update post
    POST /blog/admin/<slug>/delete — Delete post
"""
from flask import Blueprint, render_template, redirect, url_for, request, flash
import os
import glob
import re
import markdown2
import bleach

from app.auth_utils import login_required

blog_bp = Blueprint("blog_posts", __name__, url_prefix="/blog")

BLOG_DIR = os.environ.get("BLOG_DIR", "/app/blogs")
BLOG_INDEX = os.path.join(BLOG_DIR, "index.md")

# Bleach allowed tags for blog HTML
_ALLOWED_TAGS = [
    "p", "br", "strong", "em", "b", "i", "u", "s", "a", "ul", "ol", "li",
    "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "code",
    "table", "thead", "tbody", "tr", "th", "td", "hr", "div", "span",
    "img", "kbd", "sub", "sup", "abbr", "cite", "dfn", "figure",
    "figcaption", "details", "summary",
]
_ALLOWED_ATTR = {
    "a": ["href", "title", "target", "rel"],
    "img": ["src", "alt", "title"],
    "abbr": ["title"],
    "*": ["class", "id"],
}


def _sanitize_html(html: str) -> str:
    """Sanitize HTML output from markdown."""
    return bleach.clean(html, tags=_ALLOWED_TAGS, attributes=_ALLOWED_ATTR, strip=True)


def _validate_slug(slug: str) -> str:
    """Validate and normalize a blog slug. Only allows safe characters."""
    # Only allow lowercase alphanumerics and hyphens
    safe = re.sub(r'[^a-z0-9\-]', '', slug.lower())
    # Prevent path traversal
    safe = safe.replace('.', '').replace('/', '').replace('\\', '')
    return safe


def _generate_toc(html: str) -> str:
    """Extract h2/h3 headings from HTML and generate a table of contents."""
    # Find h2 and h3 tags with their text content
    headings = []
    for match in re.finditer(r'<h([23])[^>]*>(.*?)</h\1>', html, re.DOTALL):
        level = int(match.group(1))
        # Strip inner tags to get plain text
        text = re.sub(r'<[^>]+>', '', match.group(2)).strip()
        slug = re.sub(r'[^\w\s-]', '', text.lower())
        slug = re.sub(r'[\s]+', '-', slug).strip('-')
        # Add id to the heading
        headings.append({
            'level': level,
            'text': text,
            'id': f'toc-{slug}',
        })

    if not headings:
        return ""

    # Inject IDs into HTML headings
    toc_html = '<ol>'
    for i, h in enumerate(headings):
        # Inject id attribute into the heading
        tag = f'h{h["level"]}'
        html = re.sub(
            rf'<{tag}([^>]*)>',
            f'<{tag}\\1 id="{h["id"]}">',
            html,
            count=1,
        )
        if h['level'] == 2:
            toc_html += f'<li><a href="#{h["id"]}">{h["text"]}</a>'
        else:
            toc_html += f'<li class="toc-h3"><a href="#{h["id"]}">{h["text"]}</a>'

    toc_html += '</ol>'
    return toc_html, html


def _word_count(text: str) -> int:
    """Count words in HTML content."""
    plain = re.sub(r'<[^>]+>', ' ', text)
    plain = re.sub(r'\s+', ' ', plain).strip()
    return len(plain.split()) if plain else 0


def _get_posts() -> list[dict]:
    """Read all blog posts from directory."""
    posts = []
    if not os.path.exists(BLOG_DIR):
        return posts
    for fname in sorted(os.listdir(BLOG_DIR), reverse=True):
        if fname == "index.md" or not fname.endswith(".md"):
            continue
        fpath = os.path.join(BLOG_DIR, fname)
        with open(fpath, "r") as f:
            content = f.read()
        # Parse frontmatter (line-based to avoid --- in body content)
        fm = {}
        body = content
        if content.startswith("---"):
            lines = content.split("\n")
            fm_end = -1
            for i in range(1, len(lines)):
                if lines[i].strip() == "---":
                    fm_end = i
                    break
            if fm_end > 0:
                fm_raw = "\n".join(lines[1:fm_end])
                body = "\n".join(lines[fm_end+1:])
                for line in fm_raw.strip().split("\n"):
                    if ":" in line:
                        k, v = line.split(":", 1)
                        fm[k.strip()] = v.strip().strip('"').strip("'")
            else:
                fm = {"title": fname.replace(".md", ""), "date": "", "summary": "", "tags": ""}
        else:
            fm = {"title": fname.replace(".md", ""), "date": "", "summary": "", "tags": ""}
        body_html = _sanitize_html(markdown2.markdown(body.strip(), extras=["tables", "fenced-code"]))
        wc = _word_count(body_html)
        posts.append({
            "slug": fname[:-3],
            "title": fm.get("title", fname.replace(".md", "")),
            "date": fm.get("date", ""),
            "author": fm.get("author", "AgentForms"),
            "summary": fm.get("summary", ""),
            "tags": [t.strip() for t in fm.get("tags", "").split(",") if t.strip()],
            "body_html": body_html,
            "word_count": wc,
            "reading_time": max(1, round(wc / 230)),
        })
    return posts


@blog_bp.route("/")
def index():
    """Blog listing page."""
    posts = _get_posts()
    return render_template("blog_index.html", posts=posts)


@blog_bp.route("/<path:slug>")
def post(slug: str):
    """Single blog post."""
    # Validate slug - prevent path traversal
    slug = _validate_slug(slug)
    fpath = os.path.join(BLOG_DIR, slug + ".md")

    # Resolve and verify the path stays within BLOG_DIR
    real_path = os.path.realpath(fpath)
    if not real_path.startswith(os.path.realpath(BLOG_DIR)):
        return "Not found", 404

    if not os.path.exists(fpath):
        return "Not found", 404

    with open(fpath, "r") as f:
        content = f.read()

    if content.startswith("---"):
        # Parse frontmatter by finding the second standalone "---" line
        lines = content.split("\n")
        fm_end = -1
        for i in range(1, len(lines)):
            if lines[i].strip() == "---":
                fm_end = i
                break
        if fm_end > 0:
            fm_raw = "\n".join(lines[1:fm_end])
            body = "\n".join(lines[fm_end+1:])
            fm = {}
            for line in fm_raw.strip().split("\n"):
                if ":" in line:
                    k, v = line.split(":", 1)
                    fm[k.strip()] = v.strip().strip('"').strip("'")
        else:
            fm = {"title": slug.replace("-", " ").title()}
            body = content
    else:
        fm = {"title": slug.replace("-", " ").title()}
        body = content

    body_html = _sanitize_html(markdown2.markdown(body.strip(), extras=["tables", "fenced-code"]))
    wc = _word_count(body_html)

    # Generate TOC
    toc = ""
    if '<h2' in body_html or '<h3' in body_html:
        toc_result = _generate_toc(body_html)
        if isinstance(toc_result, tuple):
            toc, body_html = toc_result

    return render_template("blog_post.html", post={
        "slug": slug,
        "title": fm.get("title", slug.replace("-", " ").title()),
        "date": fm.get("date", ""),
        "author": fm.get("author", "AgentForms"),
        "summary": fm.get("summary", ""),
        "tags": [t.strip() for t in fm.get("tags", "").split(",") if t.strip()],
        "body_html": body_html,
        "toc": toc,
        "word_count": wc,
        "reading_time": max(1, round(wc / 230)),
    })


@blog_bp.route("/admin/")
@login_required
def admin_list():
    """Admin post list."""
    posts = _get_posts()
    return render_template("blog_admin.html", posts=posts, blog_dir=BLOG_DIR)


@blog_bp.route("/admin/new")
@login_required
def admin_new():
    """Create new post form."""
    return render_template("blog_edit.html", post=None, blog_dir=BLOG_DIR)


@blog_bp.route("/admin/new", methods=["POST"])
@login_required
def admin_save_new():
    """Save new post."""
    title = request.form.get("title", "").strip()
    date = request.form.get("date", "").strip()
    author = request.form.get("author", "AgentForms").strip()
    summary = request.form.get("summary", "").strip()
    tags = request.form.get("tags", "").strip()
    content = request.form.get("content", "").strip()

    if not title or not content:
        flash("Title and content are required", "error")
        return render_template("blog_edit.html", post=None, blog_dir=BLOG_DIR)

    os.makedirs(BLOG_DIR, exist_ok=True)
    slug = title.lower().replace(" ", "-").replace("&", "and").replace("'", "")
    slug = "".join(c for c in slug if c.isalnum() or c in "-")

    # Escape frontmatter values to prevent injection
    def _escape_fm(val: str) -> str:
        return val.replace("{", "{{").replace("}", "}}")

    fm = f"""---
title: {_escape_fm(title)}
date: {_escape_fm(date)}
author: {_escape_fm(author)}
summary: {_escape_fm(summary)}
tags: {_escape_fm(tags)}
---

{content}"""

    fpath = os.path.join(BLOG_DIR, slug + ".md")
    with open(fpath, "w") as f:
        f.write(fm)

    flash(f'Post "{title}" created', "success")
    return redirect(url_for("blog.admin_list"))


@blog_bp.route("/admin/<slug>/edit")
@login_required
def admin_edit(slug: str):
    """Edit existing post form."""
    slug = _validate_slug(slug)
    fpath = os.path.join(BLOG_DIR, slug + ".md")
    if not os.path.exists(fpath):
        flash("Post not found", "error")
        return redirect(url_for("blog.admin_list"))

    with open(fpath, "r") as f:
        content = f.read()

    fm = {}
    body = content
    if content.startswith("---"):
        _, fm_end = content.split("---", 1)[1].split("---", 1)
        for line in fm_end.strip().split("\n"):
            if ":" in line:
                k, v = line.split(":", 1)
                fm[k.strip()] = v.strip()
        body = content[content.find("---", content.find("---") + 3) + 3:]

    return render_template("blog_edit.html", post={
        "slug": slug,
        "title": fm.get("title", ""),
        "date": fm.get("date", ""),
        "author": fm.get("author", "AgentForms"),
        "summary": fm.get("summary", ""),
        "tags": fm.get("tags", ""),
        "content": body.strip(),
    }, blog_dir=BLOG_DIR)


@blog_bp.route("/admin/<slug>/edit", methods=["POST"])
@login_required
def admin_save_edit(slug: str):
    """Update existing post."""
    slug = _validate_slug(slug)
    title = request.form.get("title", "").strip()
    date = request.form.get("date", "").strip()
    author = request.form.get("author", "AgentForms").strip()
    summary = request.form.get("summary", "").strip()
    tags = request.form.get("tags", "").strip()
    content = request.form.get("content", "").strip()

    if not title or not content:
        flash("Title and content are required", "error")
        return redirect(request.url)

    fpath = os.path.join(BLOG_DIR, slug + ".md")

    # Escape frontmatter values to prevent injection
    def _escape_fm(val: str) -> str:
        return val.replace("{", "{{").replace("}", "}}")

    fm = f"""---
title: {_escape_fm(title)}
date: {_escape_fm(date)}
author: {_escape_fm(author)}
summary: {_escape_fm(summary)}
tags: {_escape_fm(tags)}
---

{content}"""

    with open(fpath, "w") as f:
        f.write(fm)

    flash(f'Post "{title}" updated', "success")
    return redirect(url_for("blog.admin_list"))


@blog_bp.route("/admin/<slug>/delete", methods=["POST"])
@login_required
def admin_delete(slug: str):
    """Delete post."""
    slug = _validate_slug(slug)
    fpath = os.path.join(BLOG_DIR, slug + ".md")
    if os.path.exists(fpath):
        os.remove(fpath)
        flash("Post deleted", "success")
    return redirect(url_for("blog.admin_list"))