#!/usr/bin/env python
# Minimal, robust runtime used by the CLI (daily/weekly/monthly)
from __future__ import annotations
import datetime as dt
import io, os, re
from pathlib import Path
from collections import Counter, defaultdict

SECTION_ORDER = ["Summary", "Decisions", "Actions", "Risks", "Dependencies", "Notes"]
PRIORITY_RANK = {"high": 0, "medium": 1, "low": 2}
P_EQUIV = {"p0": "high", "p1": "medium", "p2": "low"}
VALID_PRIOS = {"high", "medium", "low"}

# Bullets: -,*,+, 1./1), checkboxes [ ], [x], and common unicode bullets/dashes
BULLET_RE = re.compile(
    r'^[\s\u00A0]*('
    r'(?:[-*+])|'
    r'(?:\d+[.)])|'
    r'(?:\[[ xX\-]\])|'
    r'[\u2022\u2023\u2043\u2219\u25AA\u25AB\u25CF\u25E6\u2013\u2014]'
    r')\s+',
    re.M,
)
LEAD_TOKEN_RE = re.compile(
    r'^[\s\u00A0]*\\?(?:[-*+]|\d+[.)]|\[[ xX\-]\]|[\u2022\u2023\u2043\u2219\u25AA\u25AB\u25CF\u25E6\u2013\u2014])\s+'
)
HDR_LINE = re.compile(r'^[ \t]*#{2,6}\s*([A-Za-z][^\n#]*)$', re.M)

MOJIBAKE_FIXES = {
    "â€“": "–", "â€”": "—", "â€˜": "‘", "â€™": "’",
    "â€œ": "“", "â€\x9d": "”", "â€¢": "•",
}

def fix_mojibake(s: str) -> str:
    for k, v in MOJIBAKE_FIXES.items():
        s = s.replace(k, v)
    return s

def normalize_heading(raw: str) -> str:
    return raw.strip().split()[0].rstrip(":-—–").lower()

def slice_sections(text: str) -> dict:
    text = fix_mojibake(text.replace("\r\n", "\n"))
    sections = {}
    matches = list(HDR_LINE.finditer(text))
    if not matches:
        return sections
    for i, m in enumerate(matches):
        raw = m.group(1)
        base = normalize_heading(raw)
        start = m.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
        body = text[start:end].strip()
        for canonical in SECTION_ORDER:
            if base == canonical.lower():
                sections[canonical] = body
                break
    return sections

def collect_bullets(block: str):
    if not block:
        return []
    out = []
    for ln in block.splitlines():
        s = ln.rstrip()
        if not s.strip():
            continue
        if BULLET_RE.match(s) or LEAD_TOKEN_RE.match(s):
            out.append(s.strip())
    return out

def clean_item_text(line: str) -> str:
    s = LEAD_TOKEN_RE.sub("", line).strip()
    s = re.sub(r'^[\\]+', "", s).strip()
    return s

def strip_priority_tag(s: str) -> str:
    return re.sub(r'^\s*\[(?:high|medium|low|p0|p1|p2)\]\s*', '', s, flags=re.I)

def normalize_block_text(block: str) -> str:
    if not block:
        return ""
    lines = []
    for ln in fix_mojibake(block).splitlines():
        ln = re.sub(r'^[ \t]*\\(?=[-*+])', "", ln)
        lines.append(ln)
    return "\n".join(lines).strip()

def detect_priority(line: str):
    m = re.search(r"\[(high|medium|low|p0|p1|p2)\]", line, re.I)
    if m:
        tag = m.group(1).lower()
        label = P_EQUIV.get(tag, tag)
        return label, PRIORITY_RANK.get(label, 3)
    return "other", 3

NAME_PATTERNS = [
    re.compile(r"\]\s*([A-Z][\w.\- ]{1,40}?)\s+to\b"),
    re.compile(r"\]\s*([A-Z][\w.\- ]{1,40}?)\s+[—–-]\s+"),
    re.compile(r"\(owner:\s*([^)]+?)\)", re.I),
]
def extract_name(text: str) -> str:
    for rx in NAME_PATTERNS:
        m = rx.search(text)
        if m:
            return m.group(1).strip()
    m = re.search(r"\b([A-Z][a-zA-Z.\-]+(?:\s+[A-Z][a-zA-Z.\-]+)?)\b", text)
    return m.group(1) if m else ""

def daterange(start: dt.date, end: dt.date):
    cur = start
    while cur <= end:
        yield cur
        cur += dt.timedelta(days=1)

def aggregate_range(
    logs_dir: Path,
    start: dt.date,
    end: dt.date,
    *,
    title: str | None = None,
    group_actions: bool = True,
    flat_by_name: bool = False,
    emit_kpis: bool = False,
    owner_breakdown: bool = False,
    owner_top: int = 8,
) -> str:
    acc = {k: [] for k in SECTION_ORDER}
    action_items = []  # (rank,label,who,text)
    matched = []

    prio_counts = Counter()
    owner_counts = defaultdict(lambda: Counter({"high":0, "medium":0, "low":0}))
    decisions_count = 0
    risks_count = 0
    owners_seen = set()

    for d in daterange(start, end):
        pth = logs_dir / f"notes-{d.isoformat()}.md"
        if not pth.exists():
            continue
        matched.append(pth.as_posix())
        t = io.open(pth, "r", encoding="utf-8").read()
        secs = slice_sections(t)

        for k in ["Summary", "Decisions", "Risks", "Dependencies", "Notes"]:
            if secs.get(k):
                txt = normalize_block_text(secs[k])
                if txt:
                    acc[k].append(txt)

        if secs.get("Decisions"):
            dec_bul = collect_bullets(secs["Decisions"])
            decisions_count += len(dec_bul) if dec_bul else sum(1 for L in secs["Decisions"].splitlines() if L.strip())
        if secs.get("Risks"):
            risk_bul = collect_bullets(secs["Risks"])
            risks_count += len(risk_bul) if risk_bul else sum(1 for L in secs["Risks"].splitlines() if L.strip())

        bullets = collect_bullets(secs.get("Actions", ""))
        if not bullets and secs.get("Actions"):
            lines = [L.strip() for L in secs["Actions"].splitlines() if L.strip()]
            if any(re.search(r"\[(?:high|medium|low|p0|p1|p2)\]", L, re.I) for L in lines):
                bullets = lines
        for line in bullets:
            label, rank = detect_priority(line)
            text = clean_item_text(line)
            who  = extract_name(text).lower()
            if who: owners_seen.add(who)
            prio_counts[label] += 1
            if label in VALID_PRIOS and who:
                owner_counts[who][label] += 1
            action_items.append((rank, label, who, text))

    title = title or (f"Team Digest ({start.isoformat()} - {end.isoformat()})" if start != end else f"Team Digest ({start.isoformat()})")
    out = [f"# {title}", ""]
    out.append(
        f"_Range: {start.isoformat()} → {end.isoformat()} | Source: {logs_dir.as_posix()} | "
        f"Days matched: {len(matched)} | Actions: {len(action_items)}_"
    )
    out.append("")

    if emit_kpis:
        H,M,L = prio_counts["high"], prio_counts["medium"], prio_counts["low"]
        owners_total = len([o for o in owners_seen if o])
        out.append("## Executive KPIs")
        out.append(f"- **Actions:** {len(action_items)} (High: {H}, Medium: {M}, Low: {L})")
        out.append(f"- **Decisions:** {decisions_count}   ·   **Risks:** {risks_count}")
        out.append(f"- **Owners:** {owners_total}   ·   **Days with notes:** {len(matched)}")
        out.append("")
        if owner_breakdown and owner_counts:
            rows = []
            for owner, cc in owner_counts.items():
                total = cc["high"] + cc["medium"] + cc["low"]
                rows.append((owner, cc["high"], cc["medium"], cc["low"], total))
            rows.sort(key=lambda r: (-r[4], r[0]))
            top = rows[:owner_top]
            rest = rows[owner_top:]
            if rest:
                rh = sum(r[1] for r in rest); rm = sum(r[2] for r in rest); rl = sum(r[3] for r in rest); rt = sum(r[4] for r in rest)
                top.append(("Other", rh, rm, rl, rt))
            out.append("#### Owner breakdown (top)")
            out.append("| Owner | High | Medium | Low | Total |")
            out.append("|:------|----:|------:|---:|-----:|")
            for owner, hi, me, lo, tot in top:
                owner_disp = owner.title() if owner != "Other" else owner
                out.append(f"| {owner_disp} | {hi} | {me} | {lo} | **{tot}** |")
            out.append("")

    def emit_block(name: str):
        items = [x for x in acc[name] if x]
        if not items: return
        out.append(f"## {name}")
        for x in items:
            out.append(x); out.append("")

    emit_block("Summary")
    emit_block("Decisions")

    if action_items:
        out.append("## Actions")
        if flat_by_name:
            for _, label, who, text in sorted(action_items, key=lambda t: (t[2], t[0], t[3].lower())):
                cleaned = strip_priority_tag(text)
                tag = f"[{label}]" if label != "other" else "[other]"
                out.append(f"- {tag} {cleaned}")
            out.append("")
        elif group_actions:
            buckets = {"high": [], "medium": [], "low": [], "other": []}
            for rank, label, who, text in sorted(action_items, key=lambda t: (t[0], t[2], t[3].lower())):
                key = label if label in buckets else "other"
                buckets[key].append(f"- {strip_priority_tag(text)}")
            for head, ttl in [("high","High"), ("medium","Medium"), ("low","Low"), ("other","Other")]:
                if buckets[head]:
                    out.append(f"### {ttl} priority")
                    out.extend(buckets[head]); out.append("")
        else:
            for _, label, who, text in sorted(action_items, key=lambda t: (t[0], t[2], t[3].lower())):
                cleaned = strip_priority_tag(text)
                tag = f"[{label}]" if label != "other" else "[other]"
                out.append(f"- {tag} {cleaned}")
            out.append("")

    emit_block("Risks")
    emit_block("Dependencies")
    emit_block("Notes")

    return "\n".join(out).rstrip() + "\n"
