#!/usr/bin/env python3
"""Create OR update an item on Kanban, Dashboard, and Action Board JSON stores.

Default behavior (backwards compatible): create a new item on all boards.

Update behavior (new): if you pass --update-title and the target exists, it will:
- append the provided --body as an update note
- optionally move the Action Board card to the requested column

Usage examples:

  # Create new item on all boards
  add_to_boards.py --id <email_id> --title "..." --body "..." --area "ToAssistant" --priority "Medium"

  # Update existing items (prefer exact match)
  add_to_boards.py --id <email_id> \
    --title "ToAssistant:FW: ..." \
    --body "New context..." \
    --update-title "BDMI Finance inbox visibility / forwarding (1745Ventures)" \
    --action-move waiting

Matching mode:
- If --update-title is provided, we try exact match by title/name first.
- If not found and --fuzzy is set, we do a conservative contains-match.

Action Board column ids (current): overdue, urgent, do_now, waiting, quick_wins, someday, done_recent
"""

import argparse
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple

KANBAN_PATH = Path("/home/isthekid/.openclaw/workspace/kanban/data.json")
DASHBOARD_PATH = Path("/home/isthekid/.openclaw/workspace/dashboard/data.json")
ACTION_PATH = Path("/home/isthekid/.openclaw/workspace/action/data.json")

ACTION_COL_IDS = {
  "overdue": "overdue",
  "urgent": "urgent",
  "do_now": "do_now",
  "doing_now": "do_now",
  "do now": "do_now",
  "waiting": "waiting",
  "waiting_on_others": "waiting",
  "quick_wins": "quick_wins",
  "someday": "someday",
  "done_recent": "done_recent",
  "done": "done_recent",
  "done recently": "done_recent",
}

KANBAN_COL_IDS = {
  "todo": "todo",
  "to do": "todo",
  "doing": "doing",
  "waiting": "waiting",
  "done": "done",
}


def now_iso_et() -> str:
  return datetime.now().astimezone().replace(microsecond=0).isoformat()


def load_json(path: Path):
  return json.loads(path.read_text())


def save_json(path: Path, data):
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")


def ensure_unique_id(base_id: str, used: set[str]) -> str:
  cid = base_id
  i = 2
  while cid in used:
    cid = f"{base_id}_{i}"
    i += 1
  return cid


def collect_ids_kanban(doc) -> set[str]:
  ids = set()
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      if isinstance(c, dict) and c.get("id"):
        ids.add(c["id"])
  return ids


def collect_ids_action(doc) -> set[str]:
  ids = set()
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      if isinstance(c, dict) and c.get("id"):
        ids.add(c["id"])
  return ids


def collect_ids_dashboard(doc) -> set[str]:
  ids = set()
  for p in doc.get("projects", []) or []:
    if isinstance(p, dict) and p.get("id"):
      ids.add(p["id"])
  return ids


def normalize_ws(s: str) -> str:
  return " ".join((s or "").split()).strip()


def _find_action_card(doc, title: str, fuzzy: bool) -> Tuple[Optional[dict], Optional[dict]]:
  """Returns (card, column)"""
  t = normalize_ws(title)
  if not t:
    return None, None

  # Exact match first
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      if normalize_ws(c.get("title")) == t:
        return c, col

  if not fuzzy:
    return None, None

  tl = t.lower()
  # Conservative contains-match
  candidates = []
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      ct = normalize_ws(c.get("title"))
      if not ct:
        continue
      ctl = ct.lower()
      if tl in ctl or ctl in tl:
        candidates.append((len(ct), c, col))

  if len(candidates) == 1:
    return candidates[0][1], candidates[0][2]

  # If multiple, pick the shortest title that matches (usually most specific), but only if it is clearly better.
  if candidates:
    candidates.sort(key=lambda x: x[0])
    return candidates[0][1], candidates[0][2]

  return None, None


def _find_kanban_card(doc, title: str, fuzzy: bool) -> Tuple[Optional[dict], Optional[dict]]:
  t = normalize_ws(title)
  if not t:
    return None, None
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      if normalize_ws(c.get("title")) == t:
        return c, col
  if not fuzzy:
    return None, None
  tl = t.lower()
  candidates = []
  for col in doc.get("columns", []):
    for c in col.get("cards", []) or []:
      ct = normalize_ws(c.get("title"))
      if not ct:
        continue
      ctl = ct.lower()
      if tl in ctl or ctl in tl:
        candidates.append((len(ct), c, col))
  if candidates:
    candidates.sort(key=lambda x: x[0])
    return candidates[0][1], candidates[0][2]
  return None, None


def _find_dashboard_project(doc, name: str, fuzzy: bool) -> Optional[dict]:
  t = normalize_ws(name)
  if not t:
    return None
  for p in doc.get("projects", []) or []:
    if normalize_ws(p.get("name")) == t:
      return p
  if not fuzzy:
    return None
  tl = t.lower()
  candidates = []
  for p in doc.get("projects", []) or []:
    pn = normalize_ws(p.get("name"))
    if not pn:
      continue
    pnl = pn.lower()
    if tl in pnl or pnl in tl:
      candidates.append((len(pn), p))
  if candidates:
    candidates.sort(key=lambda x: x[0])
    return candidates[0][1]
  return None


def append_update(existing_text: str, update: str) -> str:
  update = (update or "").strip()
  if not update:
    return existing_text or ""

  stamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M %Z")
  block = f"\n\nUPDATE ({stamp}):\n{update}"

  existing_text = existing_text or ""
  if update in existing_text:
    return existing_text
  return existing_text + block


def move_card_between_columns(doc, card: dict, from_col: dict, to_col_id: str):
  if not card or not from_col:
    return
  if from_col.get("id") == to_col_id:
    return
  # remove
  from_col["cards"] = [c for c in (from_col.get("cards") or []) if c is not card]
  # add to top
  for col in doc.get("columns", []):
    if col.get("id") == to_col_id:
      col.setdefault("cards", []).insert(0, card)
      return
  raise RuntimeError(f"Action Board column '{to_col_id}' not found")


def move_kanban_card(doc, card: dict, from_col: dict, to_col_id: str):
  if not card or not from_col:
    return
  if from_col.get("id") == to_col_id:
    return
  from_col["cards"] = [c for c in (from_col.get("cards") or []) if c is not card]
  for col in doc.get("columns", []):
    if col.get("id") == to_col_id:
      col.setdefault("cards", []).insert(0, card)
      return
  raise RuntimeError(f"Kanban column '{to_col_id}' not found")


def create_new(base_id: str, title: str, body: str, area: str, priority: str):
  # Kanban
  kdoc = load_json(KANBAN_PATH)
  kid = ensure_unique_id(base_id, collect_ids_kanban(kdoc))
  notes = f"Status: New\nPriority: {priority}\n\nNotes:\n{body}"
  kcard = {"id": kid, "title": title, "due": None, "area": area, "status": "New", "priority": priority, "notes": notes}
  for col in kdoc.get("columns", []):
    if col.get("id") == "todo":
      col.setdefault("cards", []).insert(0, kcard)
      break
  else:
    raise RuntimeError("Kanban column 'todo' not found")
  kdoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
  save_json(KANBAN_PATH, kdoc)

  # Dashboard
  ddoc = load_json(DASHBOARD_PATH)
  did = ensure_unique_id(base_id, collect_ids_dashboard(ddoc))
  proj = {"id": did, "name": title, "area": area, "status": "New", "priority": priority, "nextActions": [], "blockers": [], "due": None, "notes": body}
  ddoc.setdefault("projects", []).insert(0, proj)
  ddoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
  save_json(DASHBOARD_PATH, ddoc)

  # Action Board
  adoc = load_json(ACTION_PATH)
  aid = ensure_unique_id(base_id, collect_ids_action(adoc))
  acard = {
    "id": aid,
    "title": title,
    "area": area,
    "priority": priority,
    "due": None,
    "daysUntilDue": None,
    "nextActions": [body[:4000]] if body else [],
    "blockers": [],
    "effort": "short",
    "actionOwner": "self",
    "status": "New",
    "tags": ["boards:auto"],
  }
  for col in adoc.get("columns", []):
    if col.get("id") == "waiting":
      col.setdefault("cards", []).insert(0, acard)
      break
  else:
    raise RuntimeError("Action Board column 'waiting' not found")
  adoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
  save_json(ACTION_PATH, adoc)

  return kid, did, aid


def update_existing(update_title: str, update_body: str, *, fuzzy: bool, action_move: Optional[str], kanban_move: Optional[str]):
  # Action
  adoc = load_json(ACTION_PATH)
  acard, acol = _find_action_card(adoc, update_title, fuzzy=fuzzy)
  if acard:
    # Put update in nextActions (prepend)
    stamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M %Z")
    entry = f"UPDATE ({stamp}): {update_body.strip()}".strip()
    na = acard.get("nextActions") or []
    if entry and entry not in na:
      na.insert(0, entry[:4000])
    acard["nextActions"] = na

    if action_move:
      col_id = ACTION_COL_IDS.get(action_move.strip().lower(), action_move.strip())
      move_card_between_columns(adoc, acard, acol, col_id)

    adoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
    save_json(ACTION_PATH, adoc)

  # Kanban
  kdoc = load_json(KANBAN_PATH)
  kcard, kcol = _find_kanban_card(kdoc, update_title, fuzzy=fuzzy)
  if kcard:
    kcard["notes"] = append_update(kcard.get("notes") or "", update_body)
    if kanban_move:
      col_id = KANBAN_COL_IDS.get(kanban_move.strip().lower(), kanban_move.strip())
      move_kanban_card(kdoc, kcard, kcol, col_id)
    kdoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
    save_json(KANBAN_PATH, kdoc)

  # Dashboard
  ddoc = load_json(DASHBOARD_PATH)
  proj = _find_dashboard_project(ddoc, update_title, fuzzy=fuzzy)
  if proj:
    proj["notes"] = append_update(proj.get("notes") or "", update_body)
    ddoc.setdefault("meta", {})["updatedAt"] = now_iso_et()
    save_json(DASHBOARD_PATH, ddoc)

  return {
    "actionUpdated": bool(acard),
    "kanbanUpdated": bool(kcard),
    "dashboardUpdated": bool(proj),
  }


def main():
  ap = argparse.ArgumentParser()
  ap.add_argument("--title", required=True)
  ap.add_argument("--body", required=True)
  ap.add_argument("--area", default="ToAssistant")
  ap.add_argument("--priority", default="Medium")
  ap.add_argument("--id", default=None)

  # Update mode
  ap.add_argument("--update-title", default=None, help="Existing card/project title/name to update")
  ap.add_argument("--fuzzy", action="store_true", help="Allow conservative fuzzy match if exact not found")
  ap.add_argument("--action-move", default=None, help="Move Action Board card to column (e.g., waiting, do_now, done_recent)")
  ap.add_argument("--kanban-move", default=None, help="Move Kanban card to column (todo, doing, waiting, done)")

  args = ap.parse_args()

  base_id = args.id or f"boards_{int(time.time())}"

  if args.update_title:
    res = update_existing(
      args.update_title,
      args.body,
      fuzzy=args.fuzzy,
      action_move=args.action_move,
      kanban_move=args.kanban_move,
    )

    # If nothing matched, fall back to create new (so we don't lose updates)
    if not any(res.values()):
      kid, did, aid = create_new(base_id, args.title, args.body, args.area, args.priority)
      print(json.dumps({"ok": True, "mode": "create_fallback", "ids": {"kanban": kid, "dashboard": did, "action": aid}}, indent=2))
      return

    print(json.dumps({"ok": True, "mode": "update", **res}, indent=2))
    return

  # Default create mode
  kid, did, aid = create_new(base_id, args.title, args.body, args.area, args.priority)
  print(json.dumps({"ok": True, "mode": "create", "ids": {"kanban": kid, "dashboard": did, "action": aid}}, indent=2))


if __name__ == "__main__":
  main()
