#!/usr/bin/env python3
"""Generate roadmap.org from bd.

Inverted-tree layout: each tree is rooted at an issue that nothing else
(among the open set) depends on — the "epic" or standalone goal. Its
children are the things blocking it, recursively. Diamonds in the DAG
are broken into a tree by duplicating shared deps under each parent so
emacs `[/]` cookies count whole subtrees.

Usage: python3 scripts/gen_roadmap.py [output-path]
       (default output: roadmap.org at the repo root)
"""
import json
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path


def bd_json(*args):
    """Run `bd <args>` and parse JSON from stdout."""
    cmd = ['bd', *args, '--json']
    res = subprocess.run(cmd, check=True, capture_output=True, text=True)
    return json.loads(res.stdout)


def main():
    repo = Path(__file__).resolve().parent.parent
    out_path = Path(sys.argv[1]) if len(sys.argv) > 1 else repo / 'roadmap.org'

    issues = bd_json('list')
    by_id = {i['id']: i for i in issues}
    # `bd list --all` includes closed issues — used only to resolve titles
    # of already-done dep targets so we can label them in :DONE_DEPS:.
    all_issues = bd_json('list', '--all')
    all_by_id = {i['id']: i for i in all_issues}

    # --- Build dep graph (open-issue subgraph only) ----------------------
    # child depends_on parent; in our inverted tree, parent (the goal/epic)
    # sits ABOVE its deps (children).
    tree_children = defaultdict(set)
    tree_parents = defaultdict(set)
    for i in issues:
        iid = i['id']
        for d in (i.get('dependencies') or []):
            dep = d['depends_on_id']
            if dep in by_id and iid in by_id:
                tree_children[iid].add(dep)
                tree_parents[dep].add(iid)

    def order_key(iid):
        i = by_id[iid]
        return (
            0 if i['status'] == 'in_progress' else 1,
            i['priority'],
            iid,
        )

    roots = sorted(
        [iid for iid in by_id if not tree_parents.get(iid)],
        key=order_key,
    )

    # --- Helpers ---------------------------------------------------------
    status_kw = {
        'open': 'TODO',
        'in_progress': 'DOING',
        'closed': 'DONE',
        'blocked': 'WAIT',
        'deferred': 'WAIT',
    }

    def tag(s):
        return re.sub(r'[^A-Za-z0-9_@#%]', '_', s)

    def headline_tags(i, kind):
        tags = [kind, f"P{i['priority']}", tag(i['issue_type'])]
        if i.get('assignee'):
            tags.append('@' + tag(i['assignee']))
        return ':' + ':'.join(tags) + ':'

    def fmt_date(s):
        return s.replace('T', ' ').rstrip('Z') if s else ''

    def kind_of(iid):
        return 'epic' if tree_children.get(iid) else 'standalone'

    out = []
    out.append('#+TITLE: Improvise Roadmap')
    out.append('#+AUTHOR: Edward Langley')
    out.append('#+TODO: TODO DOING WAIT | DONE')
    out.append('#+STARTUP: overview')
    out.append('#+TAGS: epic standalone P0 P1 P2 P3 P4 task feature bug')
    out.append('#+PROPERTY: COOKIE_DATA todo recursive')
    out.append('')
    out.append(
        f'Generated from ~bd list --json~. {len(issues)} open issues '
        f'organised as {len(roots)} inverted dep-trees: each root is a goal '
        'that nothing else depends on; its children are the deps blocking '
        'it. Diamonds in the DAG are duplicated so each tree stands alone.'
    )
    out.append('')

    def emit(iid, depth, path):
        i = by_id[iid]
        kind = kind_of(iid)
        kw = status_kw.get(i['status'], 'TODO')
        title = i['title'].replace('[', '(').replace(']', ')')
        stars = '*' * depth
        cookie = ' [/]' if tree_children.get(iid) else ''
        head = f'{stars} {kw} {title} ({kind}){cookie}'
        pad = max(1, 95 - len(head))
        out.append(f'{head}{" " * pad}{headline_tags(i, kind)}')

        indent = '  ' * (depth - 1) + ' '
        out.append(f'{indent}:PROPERTIES:')
        out.append(f'{indent}:ID:         {iid}')
        out.append(f'{indent}:TYPE:       {i["issue_type"]}')
        out.append(f'{indent}:PRIORITY:   P{i["priority"]}')
        out.append(f'{indent}:STATUS:     {i["status"]}')
        if i.get('assignee'):
            out.append(f'{indent}:ASSIGNEE:   {i["assignee"]}')
        if i.get('owner'):
            out.append(f'{indent}:OWNER:      {i["owner"]}')
        if i.get('created_by'):
            out.append(f'{indent}:CREATED_BY: {i["created_by"]}')
        out.append(f'{indent}:CREATED:    {fmt_date(i["created_at"])}')
        out.append(f'{indent}:UPDATED:    {fmt_date(i["updated_at"])}')
        if i.get('comment_count'):
            out.append(f'{indent}:COMMENTS:   {i["comment_count"]}')
        out.append(f'{indent}:KIND:       {kind}')
        closed_deps = sorted(
            d['depends_on_id']
            for d in (i.get('dependencies') or [])
            if (ref := all_by_id.get(d['depends_on_id'])) and ref['status'] == 'closed'
        )
        if closed_deps:
            out.append(f'{indent}:DONE_DEPS:  {", ".join(closed_deps)}')
        if iid in path:
            out.append(f'{indent}:CYCLE:      yes — descendants pruned at this node')
        out.append(f'{indent}:END:')

        if iid in path:
            return  # cycle guard

        details_stars = '*' * (depth + 1)
        section_stars = '*' * (depth + 2)
        section_indent = '  ' * (depth + 1) + ' '
        if any(i.get(k) for k in ('description', 'design', 'acceptance_criteria', 'notes')):
            out.append(f'{details_stars} Details')
            for label, key in [
                ('Description', 'description'),
                ('Design', 'design'),
                ('Acceptance Criteria', 'acceptance_criteria'),
                ('Notes', 'notes'),
            ]:
                v = i.get(key)
                if not v:
                    continue
                out.append(f'{section_stars} {label}')
                for line in v.rstrip('\n').splitlines():
                    out.append(f'{section_indent}{line}' if line else '')

        new_path = path | {iid}
        for c in sorted(tree_children.get(iid, set()), key=order_key):
            out.append('')
            emit(c, depth + 1, new_path)

    for r in roots:
        emit(r, 1, frozenset())
        out.append('')

    out_path.write_text('\n'.join(out))
    print(f'Wrote {out_path} ({len(out)} lines, {len(roots)} roots, {len(by_id)} issues)')


if __name__ == '__main__':
    main()
