#!/usr/bin/env python3
"""Reconcile the Todo app's `location` tags with master_config.

The Todo picker is a SEEDED snapshot of location tags in todo.db, not a live
read of master_config. This job keeps it in sync so pods added/removed/moved in
Config Manager propagate to the picker automatically (previously a manual DB edit).

What it does (idempotent, non-destructive):
  - ADD   a site/pod tag that exists in master_config but not in the DB.
  - UNARCHIVE a site/pod tag that reappears in master_config.
  - ARCHIVE (never delete) a site/pod tag no longer in master_config, so any
    todos still linked to it keep their chips + history. Archived tags drop out
    of the picker (todo.js getTagsForType hides archived) but still render.
  - Bump meta.version on any change so open pages auto-refresh.

What it deliberately does NOT do:
  - RENAMES. A rename (e.g. "Nate 3" -> "Nate 3B") is indistinguishable from
    delete-old + add-new, so it archives the old and adds the new. Todos on the
    old tag must be re-pointed by hand. Suspected renames are flagged in output.

Usage:
  sync_todo_locations.py [DB_PATH] [--dry-run]

Runs as `ngon` from cron. Keeps todo.db DELETE journal (www-data CGI reads it).
"""

import json
import os
import sqlite3
import sys
from datetime import datetime, timezone

DEFAULT_DB = '/opt/ngon/data/todo.db'
MASTER_CONFIG = '/opt/ngon/config/master_config.json'


def now_iso():
    return datetime.now(timezone.utc).isoformat(timespec='seconds')


def load_site_pods():
    """Return {site_key: [pod_key, ...]} from master_config (mirrors migrate_todo_tags)."""
    cfg = json.load(open(MASTER_CONFIG))
    out = {}
    for sname, s in (cfg.get('sites') or {}).items():
        pods = set()
        for g in (s.get('generator_groups') or {}).values():
            pods.update((g.get('pods') or {}).keys())
        pods.update((s.get('pods') or {}).keys())
        out[sname] = sorted(pods)
    return out


def main():
    argv = sys.argv[1:]
    dry = '--dry-run' in argv
    positional = [a for a in argv if not a.startswith('--')]
    db_path = positional[0] if positional else DEFAULT_DB

    if not os.path.exists(db_path):
        print(f"ERROR: db not found: {db_path}")
        sys.exit(1)

    site_pods = load_site_pods()
    ts = now_iso()

    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=DELETE")  # keep DELETE per CGI rule
    try:
        row = conn.execute("SELECT id FROM tag_types WHERE name='location'").fetchone()
        if not row:
            print("ERROR: no 'location' tag_type — run migrate_todo_tags.py first.")
            sys.exit(1)
        loc_type = row['id']

        # Current DB location tags
        db_tags = [dict(r) for r in conn.execute(
            "SELECT id, name, parent_id, external_key, position, archived "
            "FROM tags WHERE tag_type_id=?", (loc_type,))]
        sites_by_key = {t['external_key']: t for t in db_tags if t['parent_id'] is None}
        # pods keyed by (parent_id, external_key)
        pods_by_key = {(t['parent_id'], t['external_key']): t
                       for t in db_tags if t['parent_id'] is not None}

        added, unarchived, archived = [], [], []

        def next_site_pos():
            m = max((s['position'] for s in sites_by_key.values()), default=0.0)
            return m + 1.0

        def next_pod_pos(parent_id):
            kids = [t for t in db_tags if t['parent_id'] == parent_id]
            return max((k['position'] for k in kids), default=0.0) + 1.0

        # --- Ensure every config site + pod is present & unarchived ---
        desired_sites = set(site_pods.keys())
        desired_pods = set()  # (site_key, pod_key)

        for site, pods in site_pods.items():
            st = sites_by_key.get(site)
            if st is None:
                pos = next_site_pos()
                if not dry:
                    conn.execute(
                        "INSERT INTO tags(tag_type_id,name,parent_id,external_key,position,archived,created_by,created_at) "
                        "VALUES (?,?,?,?,?,0,'config-sync',?)",
                        (loc_type, site, None, site, pos, ts))
                    sid = conn.execute(
                        "SELECT id FROM tags WHERE tag_type_id=? AND external_key=? AND parent_id IS NULL",
                        (loc_type, site)).fetchone()['id']
                else:
                    sid = -1
                st = {'id': sid, 'position': pos, 'archived': 0}
                sites_by_key[site] = st
                db_tags.append({'id': sid, 'name': site, 'parent_id': None,
                                'external_key': site, 'position': pos, 'archived': 0})
                added.append(f"site {site}")
            elif st['archived']:
                if not dry:
                    conn.execute("UPDATE tags SET archived=0 WHERE id=?", (st['id'],))
                st['archived'] = 0
                unarchived.append(f"site {site}")

            site_id = st['id']
            for pod in pods:
                desired_pods.add((site, pod))
                pt = pods_by_key.get((site_id, pod))
                if pt is None:
                    ppos = next_pod_pos(site_id)
                    if not dry:
                        conn.execute(
                            "INSERT INTO tags(tag_type_id,name,parent_id,external_key,position,archived,created_by,created_at) "
                            "VALUES (?,?,?,?,?,0,'config-sync',?)",
                            (loc_type, pod, site_id, pod, ppos, ts))
                    pods_by_key[(site_id, pod)] = {'id': -1, 'position': ppos, 'archived': 0}
                    db_tags.append({'id': -1, 'name': pod, 'parent_id': site_id,
                                    'external_key': pod, 'position': ppos, 'archived': 0})
                    added.append(f"pod {pod} ({site})")
                elif pt['archived']:
                    if not dry:
                        conn.execute("UPDATE tags SET archived=0 WHERE id=?", (pt['id'],))
                    pt['archived'] = 0
                    unarchived.append(f"pod {pod} ({site})")

        # --- Archive location tags no longer in config (non-destructive) ---
        # Build a lookup of site tag id -> site key for pod-scope checks.
        site_id_to_key = {t['id']: t['external_key']
                          for t in db_tags if t['parent_id'] is None}
        for t in list(db_tags):
            if t['archived'] or t['id'] == -1:
                continue
            if t['parent_id'] is None:
                if t['external_key'] not in desired_sites:
                    if not dry:
                        conn.execute("UPDATE tags SET archived=1 WHERE id=?", (t['id'],))
                    archived.append(f"site {t['name']}")
            else:
                skey = site_id_to_key.get(t['parent_id'])
                if (skey, t['external_key']) not in desired_pods:
                    if not dry:
                        conn.execute("UPDATE tags SET archived=1 WHERE id=?", (t['id'],))
                    archived.append(f"pod {t['name']} ({skey})")

        changed = bool(added or unarchived or archived)
        if changed and not dry:
            conn.execute(
                "UPDATE meta SET value=CAST(CAST(value AS INTEGER)+1 AS TEXT) WHERE key='version'")
            conn.commit()

        # --- Report ---
        prefix = "[dry-run] " if dry else ""
        if not changed:
            print(f"{prefix}todo locations already in sync. No changes.")
        else:
            if added:      print(f"{prefix}ADDED:      " + ", ".join(added))
            if unarchived: print(f"{prefix}UNARCHIVED: " + ", ".join(unarchived))
            if archived:   print(f"{prefix}ARCHIVED:   " + ", ".join(archived))
            # Suspected renames: an archive + an add under the same site this run.
            arch_sites = {a.split('(')[-1].rstrip(')') for a in archived if '(' in a}
            add_sites = {a.split('(')[-1].rstrip(')') for a in added if '(' in a}
            overlap = arch_sites & add_sites
            if overlap:
                print(f"{prefix}NOTE: add+archive in same site(s) {sorted(overlap)} — "
                      f"possible rename(s); re-point todos on archived pod tags by hand.")
    finally:
        conn.close()


if __name__ == '__main__':
    main()
