"""Timeline + overview-history domain.

Port of v1 main.py — global_timeline / entity_timeline (≈6924-7124),
overview_history (≈7129-7151), get_current_overview / snapshot_overview /
load_overview_versions (≈2083-2131) — reshaped as JSON feed builders for the
v2 API (the v1 versions rendered Jinja templates; shapes here mirror the
template context 1:1).

Two views everywhere, exactly as v1:
- ``notes``  — comment rows (media + target chips), expanded across
  children/related entities when scoped.
- ``log``    — the activity_log feed (newest first, LIMIT 500), optionally
  filtered to one entity.

The v1 ``_build_filter_options`` dropdown helper is NOT ported: it was
HTML-template plumbing; the v2 SPA builds pickers from the entity-list
endpoints of the other domain routers.
"""
from __future__ import annotations

from datetime import datetime, timezone

from sqlalchemy import select
from sqlalchemy.orm import Session

from ..errors import AppError
from ..models.catalog import Species, Variety
from ..models.garden import Area, Year
from .overviews import snapshot_overview as _write_snapshot  # single source
from ..models.history import ActivityLog, OverviewVersion
from ..models.notes import FieldNote, FieldNoteTarget
from ..models.planting import Plant, Planting
from .notes import (
    _base_comment,
    _not_archived,
    _target_out,
    _targets_by_note,
    load_area_maps,
    load_comments_expanded,
    load_comments_for_target,
    target_label,
    target_url,
)

# --- error codes (timeline domain) ---
TIMELINE_UNKNOWN_ENTITY = "timeline_unknown_entity"
TIMELINE_ENTITY_NOT_FOUND = "timeline_entity_not_found"

# v1 main.py:6879-6880
TIMELINE_FILTER_TYPES = frozenset(
    {"area", "planting", "plant", "species", "variety", "station", "year"}
)
TIMELINE_TYPES_WITH_CHILDREN = frozenset({"area", "planting", "species"})

# v1 main.py:2073-2080 — entity types with an overview column.
# Values are (model, overview attribute name).
OVERVIEW_ENTITIES = {
    "area": (Area, "notes"),
    "planting": (Planting, "notes"),
    "plant": (Plant, "notes"),
    "species": (Species, "description"),
    "variety": (Variety, "description"),
    "year": (Year, "notes"),
}

LOG_LIMIT = 500


# ---------------------------------------------------------------------------
# Activity-log view
# ---------------------------------------------------------------------------

def activity_entries(
    db: Session,
    garden_id: int,
    entity_type: str | None = None,
    entity_id: int | None = None,
    limit: int = LOG_LIMIT,
) -> list[dict]:
    stmt = (
        select(ActivityLog)
        .where(ActivityLog.garden_id == garden_id)
        .order_by(ActivityLog.id.desc())
        .limit(limit)
    )
    if entity_type and entity_id:
        stmt = stmt.where(
            ActivityLog.entity_type == entity_type,
            ActivityLog.entity_id == entity_id,
        )
    out = []
    for e in db.scalars(stmt):
        out.append(
            {
                "id": e.id,
                "created_at": e.created_at,
                "category": e.category,
                "entity_type": e.entity_type,
                "entity_id": e.entity_id,
                "summary": e.summary,
                "field": e.field,
                "old_value": e.old_value,
                "new_value": e.new_value,
                "entity_url": (
                    target_url(e.entity_type, e.entity_id) if e.entity_id else ""
                ),
            }
        )
    return out


# ---------------------------------------------------------------------------
# Feeds
# ---------------------------------------------------------------------------

def _global_comments(db: Session, garden_id: int, by_id: dict, show_archived: bool) -> list[dict]:
    """Unfiltered global timeline: every BODIED field_note (captures awaiting
    processing live in the queue, not here) — v1 main.py:7000-7046."""
    stmt = (
        select(FieldNote)
        .where(
            FieldNote.garden_id == garden_id,
            FieldNote.body.isnot(None),
            FieldNote.body != "",
            # App feedback is QA channel, not diary content (NULL-safe).
            FieldNote.kind.is_distinct_from("feedback"),
        )
        .order_by(
            FieldNote.comment_date.desc().nulls_last(),
            FieldNote.created_at.desc(),
            FieldNote.id.desc(),
        )
    )
    if not show_archived:
        stmt = stmt.where(_not_archived(FieldNote.is_archived))
    fns = db.scalars(stmt).all()
    targets_map = _targets_by_note(db, [fn.id for fn in fns])
    comments = []
    for fn in fns:
        c = _base_comment(fn)
        trs = targets_map.get(fn.id, [])
        prim = next((t for t in trs if t.is_primary), None)
        if prim:
            c["primary"] = _target_out(
                db, prim.target_type, prim.target_id, by_id, is_primary=True
            )
        c["other_targets"] = [
            _target_out(db, t.target_type, t.target_id, by_id)
            for t in trs
            if not t.is_primary
        ]
        c["is_primary_for_scope"] = True
        comments.append(c)
    return comments


def global_feed(
    db: Session,
    garden_id: int,
    scope_type: str | None = None,
    scope_id: int | None = None,
    children: bool | None = None,
    related: bool = False,
    archived: bool = False,
    view: str = "notes",
) -> dict:
    """v1 global_timeline: optionally scoped, comments or activity-log view.

    ``children=None`` means "default for the scope type" (on for types that
    have children, exactly as v1's ``children is None`` branch).
    """
    if scope_type and scope_type not in TIMELINE_FILTER_TYPES:
        scope_type = None
        scope_id = None
    supports_children = scope_type in TIMELINE_TYPES_WITH_CHILDREN
    include_children = (
        supports_children if children is None else (bool(children) and supports_children)
    )
    include_related = bool(related)
    show_archived = bool(archived)
    view = view if view in ("notes", "log") else "notes"

    by_id = load_area_maps(db, garden_id, include_archived=show_archived)
    scope = None
    comments: list[dict] = []
    log_entries: list[dict] = []

    if view == "log":
        log_entries = activity_entries(db, garden_id, scope_type, scope_id)
        if scope_type and scope_id:
            scope = {
                "type": scope_type,
                "id": scope_id,
                "label": target_label(db, scope_type, scope_id, by_id),
            }
    elif scope_type and scope_id:
        # /years default: only comments posted directly ABOUT the year
        # unless related expansion was requested (v1 main.py:6989).
        primary_only_for_direct = scope_type == "year" and not include_related
        comments = load_comments_expanded(
            db,
            garden_id,
            scope_type,
            scope_id,
            include_children=include_children,
            include_related=include_related,
            by_id=by_id,
            primary_only_for_direct=primary_only_for_direct,
            include_archived=show_archived,
        )
        scope = {
            "type": scope_type,
            "id": scope_id,
            "label": target_label(db, scope_type, scope_id, by_id),
        }
    else:
        comments = _global_comments(db, garden_id, by_id, show_archived)

    return {
        "view": view,
        "scope": scope,
        "comments": comments,
        "log_entries": log_entries,
        "include_children": include_children,
        "include_related": include_related,
        "supports_children": supports_children,
        "show_archived": show_archived,
    }


def _require_overview_entity(
    db: Session, garden_id: int, entity_type: str, entity_id: int
):
    """404 for unknown types (v1) and for rows that don't exist / aren't in
    this garden (v1 checked via the label-fallback heuristic)."""
    if entity_type not in OVERVIEW_ENTITIES:
        raise AppError(TIMELINE_UNKNOWN_ENTITY, "unknown entity type", 404)
    model, _col = OVERVIEW_ENTITIES[entity_type]
    row = db.get(model, entity_id)
    if row is None:
        raise AppError(
            TIMELINE_ENTITY_NOT_FOUND, f"{entity_type} {entity_id} not found", 404
        )
    if entity_type == "variety":
        # Varieties carry no garden_id; scope through the parent species.
        if row.species_id is not None:
            sp_garden = db.scalar(
                select(Species.garden_id).where(Species.id == row.species_id)
            )
            if sp_garden is not None and sp_garden != garden_id:
                raise AppError(
                    TIMELINE_ENTITY_NOT_FOUND,
                    f"{entity_type} {entity_id} not found",
                    404,
                )
    elif getattr(row, "garden_id", garden_id) != garden_id:
        raise AppError(
            TIMELINE_ENTITY_NOT_FOUND, f"{entity_type} {entity_id} not found", 404
        )
    return row


def entity_feed(
    db: Session,
    garden_id: int,
    entity_type: str,
    entity_id: int,
    archived: bool = False,
    view: str = "notes",
) -> dict:
    """v1 entity_timeline — per-entity comment thread or activity log."""
    _require_overview_entity(db, garden_id, entity_type, entity_id)
    show_archived = bool(archived)
    view = view if view in ("notes", "log") else "notes"
    by_id = load_area_maps(db, garden_id, include_archived=show_archived)
    if view == "log":
        comments: list[dict] = []
        log_entries = activity_entries(db, garden_id, entity_type, entity_id)
    else:
        log_entries = []
        comments = load_comments_for_target(
            db, garden_id, entity_type, entity_id, by_id,
            include_archived=show_archived,
        )
    return {
        "entity_type": entity_type,
        "entity_id": entity_id,
        "label": target_label(db, entity_type, entity_id, by_id),
        "view": view,
        "comments": comments,
        "log_entries": log_entries,
        "show_archived": show_archived,
    }


# ---------------------------------------------------------------------------
# Overview versions (v1 get_current_overview / snapshot_overview /
# load_overview_versions / overview_history)
# ---------------------------------------------------------------------------

def get_current_overview(
    db: Session, garden_id: int, entity_type: str, entity_id: int
) -> str | None:
    """Current overview text, or None when the entity type is unknown or the
    row doesn't exist (v1 semantics, plus garden scoping)."""
    if entity_type not in OVERVIEW_ENTITIES:
        return None
    try:
        row = _require_overview_entity(db, garden_id, entity_type, entity_id)
    except AppError:
        return None
    _model, col = OVERVIEW_ENTITIES[entity_type]
    return getattr(row, col) or ""


def snapshot_overview(
    db: Session,
    garden_id: int,
    entity_type: str,
    entity_id: int,
    new_content: str,
    author: str = "user",
    change_note: str = "",
) -> None:
    """If the current overview differs from new_content, snapshot the CURRENT
    value into overview_versions BEFORE the caller writes the new content.
    Idempotent: silently returns for unknown entities or unchanged content.
    Called by the edit domain on every overview-column write."""
    if entity_type not in OVERVIEW_ENTITIES:
        return
    current = get_current_overview(db, garden_id, entity_type, entity_id)
    if current is None:
        return
    _write_snapshot(
        db, entity_type, entity_id, current, new_content,
        author=author, change_note=change_note or "",
    )
    db.flush()


def load_overview_versions(
    db: Session, entity_type: str, entity_id: int
) -> list[dict]:
    rows = db.scalars(
        select(OverviewVersion)
        .where(
            OverviewVersion.entity_type == entity_type,
            OverviewVersion.entity_id == entity_id,
        )
        .order_by(OverviewVersion.created_at.desc(), OverviewVersion.id.desc())
    )
    return [
        {
            "id": v.id,
            "entity_type": v.entity_type,
            "entity_id": v.entity_id,
            "content": v.content,
            "created_at": v.created_at,
            "author": v.author,
            "change_note": v.change_note or "",
        }
        for v in rows
    ]


def overview_history(
    db: Session, garden_id: int, entity_type: str, entity_id: int
) -> dict:
    """v1 overview_history route payload: current content + version list."""
    current = get_current_overview(db, garden_id, entity_type, entity_id)
    if current is None:
        # Distinguish unknown type from missing row for a precise error code.
        if entity_type not in OVERVIEW_ENTITIES:
            raise AppError(TIMELINE_UNKNOWN_ENTITY, "unknown entity type", 404)
        raise AppError(
            TIMELINE_ENTITY_NOT_FOUND, f"{entity_type} {entity_id} not found", 404
        )
    return {
        "entity_type": entity_type,
        "entity_id": entity_id,
        "label": target_label(db, entity_type, entity_id),
        "current": current,
        "versions": load_overview_versions(db, entity_type, entity_id),
    }
