#!/usr/bin/env python3
"""POST endpoint for todo mutations (tag/campaign model).

Body: JSON {action: "...", ...args}. Every mutation bumps meta.version.
Response: {success, version, ...}

Todos:      add_todo, edit_todo, delete_todo, set_status, reorder_todos,
            set_todo_tags, set_campaign
Notes:      add_note, delete_event
Tags:       add_tag, rename_tag, archive_tag, reorder_tags
Tag types:  add_tag_type
Campaigns:  add_campaign, edit_campaign, set_campaign_status, delete_campaign,
            bulk_spawn
"""

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

sys.path.insert(0, '/opt/ngon/apps')
from managers.auth_manager import AuthManager

DB = os.environ.get('TODO_DB', '/opt/ngon/data/todo.db')
VALID_STATUS = ('open', 'in_progress', 'blocked', 'done')
VALID_CAMPAIGN_STATUS = ('active', 'done', 'archived')


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


def bump_version(conn):
    conn.execute("UPDATE meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE key='version'")
    return int(conn.execute("SELECT value FROM meta WHERE key='version'").fetchone()[0])


def next_position(conn, table, where_sql="", where_args=()):
    row = conn.execute(f"SELECT COALESCE(MAX(position), 0) FROM {table} {where_sql}", where_args).fetchone()
    return float(row[0]) + 1.0


def add_event(conn, todo_id, etype, body, user, ts):
    conn.execute(
        "INSERT INTO todo_events(todo_id,type,body,created_by,created_at) VALUES (?,?,?,?,?)",
        (todo_id, etype, body, user, ts))


def tag_name(conn, tag_id):
    r = conn.execute("SELECT name FROM tags WHERE id=?", (tag_id,)).fetchone()
    return r[0] if r else f"#{tag_id}"


def respond(payload):
    print("Content-Type: application/json")
    print("")
    print(json.dumps(payload))


def main():
    auth = AuthManager('todo')
    username = auth.is_authenticated()
    if not username or not auth.check_access(username):
        respond({"success": False, "error": "unauthorized"})
        return

    try:
        length = int(os.environ.get('CONTENT_LENGTH') or 0)
    except ValueError:
        length = 0
    raw = sys.stdin.read(length) if length else ''
    try:
        body = json.loads(raw) if raw else {}
    except json.JSONDecodeError:
        respond({"success": False, "error": "invalid json"})
        return

    action = body.get('action')
    if not action:
        respond({"success": False, "error": "missing action"})
        return

    user = username
    conn = sqlite3.connect(DB)
    conn.execute("PRAGMA foreign_keys = ON")
    try:
        result = {"success": True}
        ts = now_iso()

        # ---------- todos ----------
        if action == 'add_todo':
            text = (body.get('text') or '').strip()
            if not text:
                raise ValueError("text required")
            campaign_id = body.get('campaign_id') or None
            due_date = body.get('due_date') or None
            pos = next_position(conn, 'items')
            cur = conn.execute(
                "INSERT INTO items(text,position,status,due_date,campaign_id,completed,created_by,created_at) "
                "VALUES (?,?,?,?,?,0,?,?)",
                (text, pos, 'open', due_date, campaign_id, user, ts))
            tid = cur.lastrowid
            result['item_id'] = tid
            for tag_id in (body.get('tag_ids') or []):
                conn.execute("INSERT OR IGNORE INTO todo_tags(todo_id,tag_id) VALUES (?,?)", (tid, int(tag_id)))
            add_event(conn, tid, 'created', None, user, ts)
            note = (body.get('note') or '').strip()
            if note:
                add_event(conn, tid, 'note', note, user, ts)

        elif action == 'edit_todo':
            tid = int(body['todo_id'])
            if 'text' in body:
                text = (body.get('text') or '').strip()
                if not text:
                    raise ValueError("text required")
                conn.execute("UPDATE items SET text=? WHERE id=?", (text, tid))
            if 'due_date' in body:
                conn.execute("UPDATE items SET due_date=? WHERE id=?",
                             (body.get('due_date') or None, tid))

        elif action == 'delete_todo':
            tid = int(body['todo_id'])
            conn.execute("DELETE FROM items WHERE id=?", (tid,))  # FK cascades tags+events

        elif action == 'set_status':
            tid = int(body['todo_id'])
            status = body.get('status')
            if status not in VALID_STATUS:
                raise ValueError(f"bad status: {status}")
            prev = conn.execute("SELECT status FROM items WHERE id=?", (tid,)).fetchone()
            prev = prev[0] if prev else 'open'
            if status == 'done':
                conn.execute(
                    "UPDATE items SET status=?, completed=1, completed_by=?, completed_at=? WHERE id=?",
                    (status, user, ts, tid))
            else:
                conn.execute(
                    "UPDATE items SET status=?, completed=0, completed_by=NULL, completed_at=NULL WHERE id=?",
                    (status, tid))
            if prev != status:
                add_event(conn, tid, 'status', f"{prev} → {status}", user, ts)

        elif action == 'reorder_todos':
            ids = body.get('todo_ids') or []
            for idx, tid in enumerate(ids):
                conn.execute("UPDATE items SET position=? WHERE id=?", (float(idx + 1), int(tid)))

        elif action == 'set_todo_tags':
            tid = int(body['todo_id'])
            new_ids = set(int(x) for x in (body.get('tag_ids') or []))
            cur_ids = set(r[0] for r in conn.execute(
                "SELECT tag_id FROM todo_tags WHERE todo_id=?", (tid,)))
            added = new_ids - cur_ids
            removed = cur_ids - new_ids
            for x in removed:
                conn.execute("DELETE FROM todo_tags WHERE todo_id=? AND tag_id=?", (tid, x))
            for x in added:
                conn.execute("INSERT OR IGNORE INTO todo_tags(todo_id,tag_id) VALUES (?,?)", (tid, x))
            if added or removed:
                parts = []
                if added:
                    parts.append("+" + ", +".join(tag_name(conn, x) for x in added))
                if removed:
                    parts.append("-" + ", -".join(tag_name(conn, x) for x in removed))
                add_event(conn, tid, 'tag', " ".join(parts), user, ts)

        elif action == 'set_campaign':
            tid = int(body['todo_id'])
            cid = body.get('campaign_id') or None
            conn.execute("UPDATE items SET campaign_id=? WHERE id=?", (cid, tid))

        # ---------- notes / events ----------
        elif action == 'add_note':
            tid = int(body['todo_id'])
            note = (body.get('body') or '').strip()
            if not note:
                raise ValueError("note body required")
            add_event(conn, tid, 'note', note, user, ts)

        elif action == 'delete_event':
            eid = int(body['event_id'])
            # only manual notes are deletable
            conn.execute("DELETE FROM todo_events WHERE id=? AND type='note'", (eid,))

        # ---------- tags ----------
        elif action == 'add_tag':
            type_id = int(body['tag_type_id'])
            name = (body.get('name') or '').strip()
            if not name:
                raise ValueError("name required")
            parent_id = body.get('parent_id') or None
            pos = next_position(conn, 'tags', "WHERE tag_type_id=? AND (parent_id IS ? OR parent_id=?)",
                                (type_id, parent_id, parent_id))
            cur = conn.execute(
                "INSERT INTO tags(tag_type_id,name,parent_id,external_key,position,created_by,created_at) "
                "VALUES (?,?,?,?,?,?,?)",
                (type_id, name, parent_id, None, pos, user, ts))
            result['tag_id'] = cur.lastrowid

        elif action == 'rename_tag':
            name = (body.get('name') or '').strip()
            if not name:
                raise ValueError("name required")
            conn.execute("UPDATE tags SET name=? WHERE id=?", (name, int(body['tag_id'])))

        elif action == 'delete_tag':
            # FK cascade removes todo_tags links (todos keep, lose the tag) and any child tags
            conn.execute("DELETE FROM tags WHERE id=?", (int(body['tag_id']),))

        elif action == 'archive_tag':
            conn.execute("UPDATE tags SET archived=? WHERE id=?",
                         (1 if body.get('archived') else 0, int(body['tag_id'])))

        elif action == 'reorder_tags':
            ids = body.get('tag_ids') or []
            for idx, x in enumerate(ids):
                conn.execute("UPDATE tags SET position=? WHERE id=?", (float(idx + 1), int(x)))

        # ---------- tag types ----------
        elif action == 'add_tag_type':
            name = (body.get('name') or '').strip().lower()
            label = (body.get('label') or '').strip()
            if not name or not label:
                raise ValueError("name and label required")
            color = body.get('color') or '#888888'
            pos = next_position(conn, 'tag_types')
            cur = conn.execute(
                "INSERT INTO tag_types(name,label,color,is_hierarchical,seeded_from,position) "
                "VALUES (?,?,?,0,NULL,?)", (name, label, color, pos))
            result['tag_type_id'] = cur.lastrowid

        # ---------- campaigns ----------
        elif action == 'add_campaign':
            name = (body.get('name') or '').strip()
            if not name:
                raise ValueError("name required")
            cur = conn.execute(
                "INSERT INTO campaigns(name,description,due_date,status,color,created_by,created_at) "
                "VALUES (?,?,?,'active',?,?,?)",
                (name, body.get('description') or None, body.get('due_date') or None,
                 body.get('color') or '#aa44ff', user, ts))
            result['campaign_id'] = cur.lastrowid

        elif action == 'edit_campaign':
            cid = int(body['campaign_id'])
            conn.execute(
                "UPDATE campaigns SET name=COALESCE(?,name), description=?, due_date=?, color=COALESCE(?,color) "
                "WHERE id=?",
                ((body.get('name') or '').strip() or None, body.get('description') or None,
                 body.get('due_date') or None, body.get('color') or None, cid))

        elif action == 'set_campaign_status':
            cid = int(body['campaign_id'])
            status = body.get('status')
            if status not in VALID_CAMPAIGN_STATUS:
                raise ValueError(f"bad campaign status: {status}")
            completed_at = ts if status == 'done' else None
            conn.execute("UPDATE campaigns SET status=?, completed_at=? WHERE id=?",
                         (status, completed_at, cid))

        elif action == 'delete_campaign':
            cid = int(body['campaign_id'])
            conn.execute("DELETE FROM campaigns WHERE id=?", (cid,))  # items.campaign_id -> NULL via FK

        elif action == 'bulk_spawn':
            # Create one todo per location tag, all sharing a campaign.
            text = (body.get('text') or '').strip()
            if not text:
                raise ValueError("text required")
            loc_tag_ids = [int(x) for x in (body.get('tag_ids') or [])]
            if not loc_tag_ids:
                raise ValueError("at least one location required")
            extra_tag_ids = [int(x) for x in (body.get('extra_tag_ids') or [])]
            due_date = body.get('due_date') or None

            cid = body.get('campaign_id')
            if not cid:
                cname = (body.get('campaign_name') or text).strip()
                cur = conn.execute(
                    "INSERT INTO campaigns(name,description,due_date,status,color,created_by,created_at) "
                    "VALUES (?,?,?,'active',?,?,?)",
                    (cname, body.get('description') or None, due_date,
                     body.get('color') or '#aa44ff', user, ts))
                cid = cur.lastrowid
            cid = int(cid)
            result['campaign_id'] = cid

            spawned = []
            for loc in loc_tag_ids:
                pos = next_position(conn, 'items')
                cur = conn.execute(
                    "INSERT INTO items(text,position,status,due_date,campaign_id,completed,created_by,created_at) "
                    "VALUES (?,?,?,?,?,0,?,?)",
                    (text, pos, 'open', due_date, cid, user, ts))
                tid = cur.lastrowid
                conn.execute("INSERT OR IGNORE INTO todo_tags(todo_id,tag_id) VALUES (?,?)", (tid, loc))
                for x in extra_tag_ids:
                    conn.execute("INSERT OR IGNORE INTO todo_tags(todo_id,tag_id) VALUES (?,?)", (tid, x))
                add_event(conn, tid, 'created', None, user, ts)
                spawned.append(tid)
            result['spawned'] = spawned

        else:
            raise ValueError(f"unknown action: {action}")

        result['version'] = bump_version(conn)
        conn.commit()
        respond(result)
    except Exception as e:
        conn.rollback()
        respond({"success": False, "error": str(e)})
    finally:
        conn.close()


if __name__ == "__main__":
    main()
