"""The editorial commands: articles in, subscribers out.

Registered into `app.cli` by one line so the content commands stay in their own file
(Stream D owns this one, Stream A owns `cli.py`). Rules and shapes: `services/editorial.py`,
`services/subscribers.py`, `models/editorial.py`.

    python -m app.cli articles import <file> [--kind article|airport_writeup|category_intro]
        [--airport IATA] [--category NAME] [--slug S] [--title T] [--standfirst S]
        [--author NAME] [--hero-image URL] [--publish]
    python -m app.cli articles import --upload <id> ...    # one client hand-in from /todo
    python -m app.cli articles import --todo <id> ...      # every document handed in on one to-do
    python -m app.cli articles list [--all]
    python -m app.cli articles publish <slug> [<slug> ...]
    python -m app.cli articles unpublish <slug> [<slug> ...]
    python -m app.cli subscribers export [--out FILE] [--include-unsubscribed]
    python -m app.cli subscribers stats
    python -m app.cli subscribers unsubscribe <email>

Imports are drafts by default and idempotent on slug: run the same command twice and the
second run replaces the text without touching the status. Word files are converted by
`mammoth`; embedded images are counted and left out (they arrive through the shared Drive).
The export is the only way the list leaves the database; it runs here, in the container's
shell, and never through a route.
"""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

from sqlalchemy import select

from app.db import SessionLocal
from app.models.catalog import Account
from app.models.client import ClientUpload
from app.models.editorial import ARTICLE_KINDS, Article
from app.services import editorial, subscribers
from app.services.uploads import UPLOAD_ROOT


def _author_id(db, name: str | None) -> int | None:
    if not name:
        return None
    account = db.scalar(select(Account).where(Account.display_name == name))
    if account is None:
        known = ", ".join(a.display_name for a in db.scalars(select(Account).order_by(Account.id)))
        raise SystemExit(f"no account named {name!r} (known: {known or 'none'})")
    return account.id


def _sources(db, args: argparse.Namespace) -> list[tuple[bytes, str, str | None, int | None]]:
    """(bytes, name, note, upload_id) for each document the command names."""
    if args.file:
        path = Path(args.file)
        if not path.is_file():
            raise SystemExit(f"not a file: {path}")
        return [(path.read_bytes(), path.name, None, None)]
    if args.upload:
        rows = [db.get(ClientUpload, args.upload)]
        if rows[0] is None:
            raise SystemExit(f"no upload with id {args.upload}")
    else:
        rows = list(
            db.scalars(
                select(ClientUpload)
                .where(ClientUpload.todo_id == args.todo)
                .order_by(ClientUpload.created_at)
            )
        )
        if not rows:
            raise SystemExit(f"to-do {args.todo} has no uploads")
    out = []
    for u in rows:
        if u.kind == "image" or u.content_type == "application/pdf":
            print(
                f"skipping upload {u.id} ({u.original_name}): {u.content_type} is not text we can import"
            )
            continue
        path = UPLOAD_ROOT / str(u.todo_id) / u.stored_name
        if not path.is_file():
            print(f"skipping upload {u.id} ({u.original_name}): file missing on disk")
            continue
        out.append((path.read_bytes(), u.original_name, u.note, u.id))
    return out


def cmd_articles_import(args: argparse.Namespace) -> int:
    if not (args.file or args.upload or args.todo):
        raise SystemExit("name a <file>, --upload <id> or --todo <id>")
    if args.todo and (args.slug or args.title):
        raise SystemExit("--slug and --title apply to one document; import a --todo without them")
    with SessionLocal() as db:
        author_id = _author_id(db, args.author)
        sources = _sources(db, args)
        if not sources:
            return 1
        for data, name, note, upload_id in sources:
            doc = editorial.read_document(data, name)
            try:
                result = editorial.upsert_article(
                    db,
                    doc,
                    kind=args.kind,
                    slug=args.slug,
                    title=args.title or (None if doc.title else note),
                    standfirst=args.standfirst,
                    airport_code=args.airport,
                    category=args.category,
                    author_id=author_id,
                    hero_image=args.hero_image,
                    source_upload_id=upload_id,
                    publish=args.publish,
                    fallback_name=name,
                )
            except ValueError as exc:
                print(f"{name}: {exc}", file=sys.stderr)
                return 2
            verb = "created" if result.created else "updated"
            line = f'{verb} {result.slug} ({result.status}) "{result.title}" from {name}'
            if result.dropped_images:
                line += f"; {result.dropped_images} embedded image(s) left out (place them under public/ and reference them)"
            print(line)
            for warning in result.warnings:
                print(f"  note: {warning}")
    return 0


def cmd_articles_list(args: argparse.Namespace) -> int:
    with SessionLocal() as db:
        stmt = select(Article).order_by(
            Article.kind, Article.status.desc(), Article.updated_at.desc()
        )
        if not args.all:
            stmt = stmt.where(Article.status == "published")
        rows = db.scalars(stmt).all()
        if not rows:
            print("no articles" + ("" if args.all else " published (use --all for drafts)"))
            return 0
        for a in rows:
            key = a.airport_code or a.category or ""
            when = a.published_at.strftime("%Y-%m-%d") if a.published_at else "-"
            print(f'{a.status:9s} {a.kind:16s} {key:12s} {when}  {a.slug}  "{a.title}"')
    return 0


def _set_status(slugs: list[str], status: str) -> int:
    code = 0
    with SessionLocal() as db:
        for slug in slugs:
            row = editorial.article_by_slug(db, slug, published_only=False)
            if row is None:
                print(f"no article with slug {slug}", file=sys.stderr)
                code = 1
                continue
            editorial.set_status(row, status)
            db.commit()
            print(f"{slug}: {status}")
    return code


def cmd_articles_publish(args: argparse.Namespace) -> int:
    return _set_status(args.slug, "published")


def cmd_articles_unpublish(args: argparse.Namespace) -> int:
    return _set_status(args.slug, "draft")


def cmd_subscribers_export(args: argparse.Namespace) -> int:
    with SessionLocal() as db:
        text = subscribers.export_csv(db, include_unsubscribed=args.include_unsubscribed)
    if args.out:
        path = Path(args.out)
        # Created mode 600 from the first byte, never through a symlink: this file is the list.
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
        with os.fdopen(fd, "w") as fh:
            fh.write(text)
        os.chmod(path, 0o600)
        print(f"wrote {text.count(chr(10)) - 1} row(s) to {path}")
    else:
        sys.stdout.write(text)
    return 0


def cmd_subscribers_stats(_: argparse.Namespace) -> int:
    with SessionLocal() as db:
        for key, value in subscribers.stats(db).items():
            print(f"{key:24s} {value}")
    return 0


def cmd_subscribers_unsubscribe(args: argparse.Namespace) -> int:
    with SessionLocal() as db:
        try:
            changed = subscribers.unsubscribe(db, args.email)
        except subscribers.InvalidSubscription as exc:
            raise SystemExit(str(exc)) from exc
    print("unsubscribed" if changed else "no active subscription for that address")
    return 0


def register(sub: argparse._SubParsersAction) -> None:
    articles = sub.add_parser("articles", help="editorial text: import hand-ins, list, publish")
    asub = articles.add_subparsers(dest="articles_command", required=True)

    imp = asub.add_parser(
        "import", help="a Markdown, text or Word file into the articles table (draft)"
    )
    imp.add_argument("file", nargs="?", help="path to a .md, .txt or .docx file")
    imp.add_argument("--upload", type=int, help="a client_uploads id from /todo instead of a path")
    imp.add_argument("--todo", type=int, help="every document handed in on this to-do id")
    imp.add_argument("--kind", default="article", choices=ARTICLE_KINDS)
    imp.add_argument("--airport", help="IATA code (airport_writeup)")
    imp.add_argument("--category", help="our category name (category_intro)")
    imp.add_argument("--slug", help="override the slug derived from the title")
    imp.add_argument("--title", help="override the document's own title")
    imp.add_argument("--standfirst", help="the one-line summary under the title")
    imp.add_argument("--author", help="account display name (e.g. rian)")
    imp.add_argument("--hero-image", help="site-relative or https image address")
    imp.add_argument(
        "--publish", action="store_true", help="publish now instead of leaving a draft"
    )
    imp.set_defaults(func=cmd_articles_import)

    lst = asub.add_parser("list", help="published articles (--all includes drafts)")
    lst.add_argument("--all", action="store_true")
    lst.set_defaults(func=cmd_articles_list)

    pub = asub.add_parser("publish", help="make articles public")
    pub.add_argument("slug", nargs="+")
    pub.set_defaults(func=cmd_articles_publish)

    unpub = asub.add_parser("unpublish", help="take articles back to draft")
    unpub.add_argument("slug", nargs="+")
    unpub.set_defaults(func=cmd_articles_unpublish)

    subs = sub.add_parser("subscribers", help="email capture: export the list (owner only), counts")
    ssub = subs.add_subparsers(dest="subscribers_command", required=True)

    exp = ssub.add_parser(
        "export", help="CSV of active subscribers to stdout or --out FILE (mode 600)"
    )
    exp.add_argument("--out", help="write here instead of stdout")
    exp.add_argument("--include-unsubscribed", action="store_true")
    exp.set_defaults(func=cmd_subscribers_export)

    st = ssub.add_parser("stats", help="counts by source; no addresses")
    st.set_defaults(func=cmd_subscribers_stats)

    uns = ssub.add_parser("unsubscribe", help="record a withdrawal for one address")
    uns.add_argument("email")
    uns.set_defaults(func=cmd_subscribers_unsubscribe)
