#!/usr/bin/env python3
import argparse
import curses
import json
import os
import pathlib
import subprocess
import textwrap
import time
import urllib.error
import urllib.request


TABS = ["Dashboard", "Missions", "Active", "Community"]


def fetch_json(url):
    request = urllib.request.Request(url, headers={"accept": "application/json"})
    with urllib.request.urlopen(request, timeout=12) as response:
        return json.loads(response.read().decode("utf-8"))


def post_json(url, payload):
    data = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(
        url,
        data=data,
        headers={"content-type": "application/json", "accept": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=12) as response:
        return json.loads(response.read().decode("utf-8"))


def clean(value):
    return " ".join(str(value or "").split())


def clip(value, width):
    text = clean(value)
    if width <= 1:
        return ""
    if len(text) <= width:
        return text
    return text[: max(0, width - 1)] + "~"


def pad(value, width):
    text = clip(value, width)
    return text + (" " * max(0, width - len(text)))


def wrap(value, width):
    text = clean(value)
    if not text:
        return [""]
    return textwrap.wrap(text, width=max(10, width)) or [""]


def bar(value, total, width):
    if total <= 0:
        return "." * width
    filled = max(0, min(width, round((value / total) * width)))
    return "#" * filled + "." * (width - filled)


def workspace_slug(path):
    context_file = path / "context" / "action.json"
    try:
        payload = json.loads(context_file.read_text())
        return payload.get("action", {}).get("slug") or path.name.replace(
            "future-union-mission-", ""
        )
    except Exception:
        return path.name.replace("future-union-mission-", "")


class ControlRoom:
    def __init__(self, screen, base_url, runner_path):
        self.screen = screen
        self.base_url = base_url.rstrip("/")
        self.runner_path = runner_path
        self.tab = 0
        self.selected_mission = 0
        self.selected_action = 0
        self.selected_contribution = 0
        self.active_mission_slug = ""
        self.message = "Pick a mission. Open Active. Run one useful packet."
        self.actions = []
        self.missions = []
        self.contributions = []
        self.runs = []
        self.priority = {}
        self.loaded_at = ""
        self.pending_claim_slug = ""
        self.refresh()

    def refresh(self):
        try:
            payload = fetch_json(f"{self.base_url}/api/mission-computer.json")
            self.priority = payload.get("priority", {}) or {}
            self.actions = [
                action for action in payload.get("actions", []) if action.get("status") != "shipped"
            ]
            self.missions = payload.get("missions", [])
            self.contributions = payload.get("contributions", [])
        except Exception as error:
            self.message = f"Could not load Mission Computer: {error}"
            self.actions = []
            self.missions = []
            self.contributions = []
            self.priority = {}

        self.scan_runs()
        self.loaded_at = time.strftime("%H:%M:%S")
        if self.missions and not self.active_mission_slug:
            self.active_mission_slug = self.missions[0].get("slug", "")
        self.selected_mission = min(self.selected_mission, max(0, len(self.missions) - 1))
        self.selected_action = min(self.selected_action, max(0, len(self.active_actions()) - 1))
        self.selected_contribution = min(
            self.selected_contribution, max(0, len(self.contributions) - 1)
        )

    def scan_runs(self):
        runs = []
        for path in pathlib.Path.cwd().glob("future-union-mission-*"):
            if not path.is_dir():
                continue
            slug = workspace_slug(path)
            output = path / "ACTION_PROPOSAL.md" if slug == "action-idea-intake" else path / "PACKET.md"
            sources = path / "SOURCES.csv"
            source_count = 0
            if sources.exists():
                try:
                    source_count = max(0, len(sources.read_text().splitlines()) - 1)
                except Exception:
                    source_count = 0
            runs.append(
                {
                    "path": path,
                    "slug": slug,
                    "name": path.name,
                    "has_output": output.exists() and output.stat().st_size > 120,
                    "source_count": source_count,
                    "has_share": (path / "SHARE_MANIFEST.md").exists(),
                    "has_radicle": (path / "RADICLE.md").exists(),
                    "has_room": (path / ".future-union" / "rooms" / f"{slug}.md").exists(),
                }
            )
        self.runs = sorted(runs, key=lambda run: run["name"])

    def attr(self, name):
        return {
            "header": curses.A_BOLD,
            "accent": curses.A_BOLD,
            "muted": curses.A_DIM,
            "selected": curses.A_REVERSE | curses.A_BOLD,
            "warn": curses.A_BOLD,
            "box": curses.A_NORMAL,
            "success": curses.A_BOLD,
            "rule": curses.A_DIM,
        }.get(name, curses.A_NORMAL)

    def add(self, y, x, text, attr=0):
        height, width = self.screen.getmaxyx()
        if y < 0 or y >= height or x >= width:
            return
        self.screen.addstr(y, x, clip(text, width - x - 1), attr)

    def cell(self, y, x, text, width, attr=0):
        height, term_width = self.screen.getmaxyx()
        if y < 0 or y >= height or x >= term_width or width <= 0:
            return
        self.screen.addstr(y, x, pad(text, min(width, term_width - x - 1)), attr)

    def add_wrapped(self, y, x, text, width, attr=0, max_rows=4):
        rows = wrap(text, width)[:max_rows]
        for offset, line in enumerate(rows):
            self.cell(y + offset, x, line, width, attr)
        return y + len(rows)

    def active_mission(self):
        if self.tab == 1 and self.missions:
            return self.missions[self.selected_mission]
        if self.active_mission_slug:
            mission = next((item for item in self.missions if item.get("slug") == self.active_mission_slug), None)
            if mission:
                return mission
        return self.missions[0] if self.missions else None

    def active_actions(self):
        mission = self.active_mission()
        if not mission:
            return self.actions
        slug = mission.get("slug")
        return [
            action
            for action in self.actions
            if (action.get("parentMissionSlug") or action.get("missionSlug")) == slug
        ]

    def action_at_cursor(self):
        actions = self.active_actions()
        if actions:
            return actions[min(self.selected_action, len(actions) - 1)]
        return self.actions[0] if self.actions else None

    def default_slug(self):
        return clean(
            self.priority.get("agentDefaultActionSlug")
            or self.priority.get("publicDefaultActionSlug")
            or self.priority.get("reviewerDefaultActionSlug")
        )

    def action_by_slug(self, slug):
        if not slug:
            return None
        return next((action for action in self.actions if action.get("slug") == slug), None)

    def default_action(self):
        return self.action_by_slug(self.default_slug()) or self.action_at_cursor()

    def run_for_action(self, action):
        if not action:
            return None
        slug = action.get("slug")
        return next((run for run in self.runs if run["slug"] == slug), None)

    def contributions_for_action(self, action):
        if not action:
            return []
        slug = action.get("slug")
        mission_slug = action.get("parentMissionSlug") or action.get("missionSlug")
        return [
            item
            for item in self.contributions
            if item.get("actionSlug") == slug
            or (not item.get("actionSlug") and item.get("missionSlug") == mission_slug)
        ]

    def contribution_counts(self, action):
        related = self.contributions_for_action(action)
        return {
            "claimed": sum(1 for item in related if item.get("status") == "claimed"),
            "reviewed": sum(
                1 for item in related if item.get("status") in {"reviewed", "credited", "used"}
            ),
            "radicle": sum(
                1
                for item in related
                if item.get("radiclePatchId")
                or item.get("radicleHref")
                or item.get("radicleExperimentIds")
            ),
        }

    def draw(self):
        self.screen.erase()
        height, width = self.screen.getmaxyx()
        self.draw_header(width)
        if height < 22 or width < 76:
            self.draw_narrow(height, width)
        elif self.tab == 0:
            self.draw_dashboard(height, width)
        elif self.tab == 1:
            self.draw_missions(height, width)
        elif self.tab == 2:
            self.draw_active(height, width)
        else:
            self.draw_community(height, width)
        self.draw_footer(height, width)
        self.screen.refresh()

    def draw_header(self, width):
        self.cell(0, 0, " FUTURE UNION CONTROL ROOM ", 28, self.attr("header"))
        self.cell(0, 31, self.base_url, max(20, width - 54), self.attr("muted"))
        self.cell(0, width - 18, f"updated {self.loaded_at}", 17, self.attr("muted"))
        x = 2
        for index, label in enumerate(TABS):
            token = f"{index + 1} {label}"
            attr = self.attr("selected") if index == self.tab else self.attr("box")
            self.cell(2, x, f" {token} ", len(token) + 2, attr)
            x += len(token) + 4
        self.cell(3, 0, "-" * (width - 1), width - 1, self.attr("rule"))

    def draw_footer(self, height, width):
        keys = "d default  j/k move  Enter open/run  c claim  o room  p preview  P publish  r refresh  q quit"
        self.cell(height - 3, 0, "-" * (width - 1), width - 1, self.attr("rule"))
        self.cell(height - 2, 1, keys, width - 2, self.attr("muted"))
        self.cell(height - 1, 1, self.message, width - 2, self.attr("warn") if "failed" in self.message.lower() else 0)

    def draw_narrow(self, height, width):
        self.add(5, 2, "Compact view. Widen the terminal for mission detail.", self.attr("warn"))
        if self.tab == 1:
            items = self.missions
            selected = self.selected_mission
        elif self.tab == 2:
            items = self.active_actions()
            selected = self.selected_action
        elif self.tab == 3:
            items = self.contributions
            selected = self.selected_contribution
        else:
            items = self.missions
            selected = self.selected_mission
        for row, item in enumerate(items[: height - 10], start=7):
            marker = ">" if row - 7 == selected else " "
            self.add(row, 2, f"{marker} {item.get('title')}", self.attr("selected") if marker == ">" else 0)

    def draw_dashboard(self, height, width):
        left_width = min(64, max(42, width // 2 - 3))
        right_x = left_width + 5
        right_width = width - right_x - 2
        total = len(self.actions)
        claimed = sum(1 for item in self.contributions if item.get("status") == "claimed")
        reviewed = sum(1 for item in self.contributions if item.get("status") in {"reviewed", "credited", "used"})
        self.add(5, 2, "Dashboard", self.attr("header"))
        default_action = self.default_action()
        self.add(7, 2, "Default mission", self.attr("header"))
        if default_action:
            self.add_wrapped(8, 2, f"{default_action.get('title')}: press d to put compute into the current bottleneck.", left_width, self.attr("accent"), 3)
        else:
            self.add_wrapped(8, 2, "Press d after the mission feed loads.", left_width, self.attr("muted"), 2)
        self.add_wrapped(12, 2, "Make useful work legible: pick a mission, run a bounded evidence pack, let review turn it into public value.", left_width)
        y = 16
        for label, value, total_value in [
            ("Open actions", total, max(total, 1)),
            ("Active claims", claimed, max(total, 1)),
            ("Reviewed packets", reviewed, max(len(self.contributions), 1)),
            ("Local runs here", len(self.runs), max(total, 1)),
        ]:
            self.cell(y, 2, f"{label:<18} {bar(value, total_value, 18)} {value}", left_width)
            y += 1
        self.add(y + 2, 2, "Next: press d for default, or 2 to choose manually.", self.attr("accent"))

        self.add(5, right_x, "Queue signal", self.attr("header"))
        lanes = {}
        for action in self.actions:
            lane = action.get("columnTitle") or "Queue"
            lanes[lane] = lanes.get(lane, 0) + 1
        y = 7
        for lane, count in lanes.items():
            self.cell(y, right_x, f"{lane:<16} {bar(count, total, 12)} {count}", right_width, self.attr("muted"))
            y += 1
        y += 2
        self.add(y, right_x, "Local", self.attr("header"))
        self.cell(y + 2, right_x, f"runs      {len(self.runs)}", right_width, self.attr("muted"))
        self.cell(y + 3, right_x, f"packets   {sum(1 for run in self.runs if run['has_output'])}", right_width, self.attr("muted"))
        self.cell(y + 4, right_x, f"previews  {sum(1 for run in self.runs if run['has_share'])}", right_width, self.attr("muted"))

    def draw_missions(self, height, width):
        left_width = min(62, max(40, width // 2 - 4))
        right_x = left_width + 5
        right_width = width - right_x - 2
        self.add(5, 2, "Missions", self.attr("header"))
        row = 7
        for index, mission in enumerate(self.missions[: max(1, (height - 10) // 3)]):
            selected = index == self.selected_mission
            attr = self.attr("selected") if selected else self.attr("header")
            self.cell(row, 2, mission.get("title", ""), left_width, attr)
            self.cell(row + 1, 4, f"{mission.get('liveActionCount', 0)} live / {mission.get('progressPercent', 0)}%", left_width - 2, self.attr("muted"))
            row += 3
        mission = self.active_mission()
        if mission:
            self.draw_mission_detail(mission, right_x, 5, right_width, height - 9)

    def draw_mission_detail(self, mission, x, y, width, height):
        self.cell(y, x, mission.get("title", ""), width, self.attr("header"))
        y += 2
        y = self.add_wrapped(y, x, mission.get("summary"), width, self.attr("muted"), 5) + 1
        self.cell(y, x, f"Status: {mission.get('status')} / {mission.get('timeEstimate')}", width, self.attr("muted"))
        y += 2
        self.cell(y, x, f"Progress {bar(mission.get('progressPercent', 0), 100, 22)} {mission.get('progressPercent', 0)}%", width)
        y += 3
        self.cell(y, x, "Enter opens this mission in Active.", width, self.attr("accent"))
        live = mission.get("liveActions", [])
        if live:
            y += 3
            self.cell(y, x, "Runnable cards", width, self.attr("header"))
            y += 2
            for action in live[: max(0, height - (y - 5))]:
                self.cell(y, x, f"- {action.get('title')}", width, self.attr("muted"))
                y += 1

    def draw_active(self, height, width):
        actions = self.active_actions()
        mission = self.active_mission()
        left_width = min(66, max(42, width // 2 - 4))
        right_x = left_width + 5
        right_width = width - right_x - 2
        title = f"Active: {mission.get('title')}" if mission else "Active"
        self.add(5, 2, title, self.attr("header"))
        if not actions:
            self.add(7, 2, "No active cards for this mission yet.", self.attr("muted"))
            return
        row = 7
        for index, action in enumerate(actions[: max(1, (height - 10) // 3)]):
            counts = self.contribution_counts(action)
            run = self.run_for_action(action)
            selected = index == self.selected_action
            attr = self.attr("selected") if selected else self.attr("header")
            state = "local" if run else "ready"
            self.cell(row, 2, action.get("title", ""), left_width - 14, attr)
            self.cell(row, left_width - 10, state, 9, self.attr("muted"))
            meta = f"{action.get('timeNeeded')} / {counts['claimed']} active / {counts['reviewed']} reviewed"
            self.cell(row + 1, 4, meta, left_width - 4, self.attr("muted"))
            row += 3
        self.draw_action_detail(self.action_at_cursor(), right_x, 5, right_width, height - 9)

    def draw_action_detail(self, action, x, y, width, height):
        if not action:
            return
        counts = self.contribution_counts(action)
        run = self.run_for_action(action)
        self.cell(y, x, action.get("title", ""), width, self.attr("header"))
        y += 2
        y = self.add_wrapped(y, x, action.get("summary"), width, self.attr("muted"), 5) + 1
        self.cell(y, x, f"Mission: {action.get('parentMissionTitle')}", width, self.attr("muted"))
        y += 1
        self.cell(y, x, f"Time: {action.get('timeNeeded')} / {action.get('progress')}", width, self.attr("muted"))
        y += 2
        next_action = "run"
        if run and not run["has_output"]:
            next_action = "finish packet"
        elif run and not run["has_share"]:
            next_action = "preview packet"
        elif run:
            next_action = "review"
        self.cell(y, x, f"Next: {next_action}", width, self.attr("accent"))
        y += 2
        self.cell(y, x, f"Community: {counts['claimed']} active / {counts['reviewed']} reviewed / {counts['radicle']} Radicle", width)
        if run:
            y += 2
            states = [
                "packet" if run["has_output"] else "draft",
                f"{run['source_count']} sources",
                "preview" if run["has_share"] else "no preview",
                "room" if run["has_room"] else "no room",
            ]
            self.cell(y, x, "Local run: " + " / ".join(states), width, self.attr("muted"))

    def draw_community(self, height, width):
        left_width = min(72, max(42, width // 2))
        right_x = left_width + 5
        right_width = width - right_x - 2
        self.add(5, 2, "Community", self.attr("header"))
        if not self.contributions:
            self.add(7, 2, "No public contribution records loaded yet.", self.attr("muted"))
            return
        row = 7
        for index, item in enumerate(self.contributions[: max(1, (height - 10) // 3)]):
            selected = index == self.selected_contribution
            attr = self.attr("selected") if selected else self.attr("header")
            self.cell(row, 2, item.get("title", ""), left_width, attr)
            self.cell(row + 1, 4, f"{item.get('status')} / {item.get('contributor')}", left_width - 2, self.attr("muted"))
            row += 3
        item = self.contributions[self.selected_contribution]
        self.cell(5, right_x, item.get("title", ""), right_width, self.attr("header"))
        y = 7
        y = self.add_wrapped(y, right_x, item.get("summary"), right_width, self.attr("muted"), 5) + 1
        self.cell(y, right_x, f"Status: {item.get('status')} / {item.get('outputType')}", right_width, self.attr("muted"))
        y += 2
        self.add_wrapped(y, right_x, "Credit is proportional to reviewed usefulness, not noisy activity.", right_width, self.attr("accent"), 3)

    def selected_workspace(self):
        return self.run_for_action(self.action_at_cursor())

    def run_command(self, args, cwd=None, env=None):
        curses.def_prog_mode()
        curses.endwin()
        try:
            print("\n$ " + " ".join(str(arg) for arg in args) + "\n")
            subprocess.run(args, cwd=cwd, env=env or os.environ.copy(), check=False)
            input("\nPress Enter to return to Future Union...")
        finally:
            curses.reset_prog_mode()
            self.screen.refresh()
            self.refresh()

    def open_or_run(self):
        if self.tab == 0:
            self.tab = 1
            self.message = "Choose a mission, then Enter."
            return
        if self.tab == 1:
            mission = self.active_mission()
            if mission:
                self.active_mission_slug = mission.get("slug", "")
                self.selected_action = 0
                self.tab = 2
                self.message = f"Active mission: {mission.get('title')}"
            return
        if self.tab != 2:
            self.message = "Switch to Missions or Active to run work."
            return
        action = self.action_at_cursor()
        if not action:
            return
        env = os.environ.copy()
        env["FU_PICK"] = "0"
        env["FU_BASE_URL"] = self.base_url
        self.run_command(["sh", self.runner_path, action.get("slug")], env=env)

    def run_default(self):
        action = self.default_action()
        if not action:
            self.message = "No default mission is available yet."
            return
        env = os.environ.copy()
        env["FU_PICK"] = "0"
        env["FU_BASE_URL"] = self.base_url
        self.run_command(["sh", self.runner_path, action.get("slug")], env=env)

    def claim_action(self):
        action = self.action_at_cursor()
        if not action:
            return
        if self.pending_claim_slug != action.get("slug"):
            self.pending_claim_slug = action.get("slug")
            self.message = f"Press c again to record an active claim for {action.get('title')}."
            return
        payload = {
            "consent": "yes",
            "missionSlug": action.get("parentMissionSlug") or action.get("missionSlug"),
            "actionSlug": action.get("slug"),
            "actionTitle": action.get("title"),
            "missionTitle": action.get("parentMissionTitle"),
            "contributorName": os.environ.get("FU_CONTRIBUTOR_NAME", ""),
            "contributorHandle": os.environ.get("FU_CONTRIBUTOR_HANDLE", ""),
            "contact": os.environ.get("FU_CONTACT", ""),
            "anonymous": os.environ.get("FU_ANONYMOUS", "yes"),
            "agent": os.environ.get("FU_AGENT", "local runner"),
        }
        try:
            post_json(f"{self.base_url}/api/contributions/claims", payload)
            self.message = "Claim recorded. Reviewed credit still needs a useful packet."
            self.pending_claim_slug = ""
            self.refresh()
        except urllib.error.HTTPError as error:
            self.message = f"Claim failed: HTTP {error.code}"
        except Exception as error:
            self.message = f"Claim failed: {error}"

    def workspace_command(self, command):
        run = self.selected_workspace()
        if not run:
            self.message = "No local workspace yet. Run the action first."
            return
        helper = run["path"] / "bin" / "fu-mission"
        if not helper.exists():
            self.message = f"Missing helper in {run['name']}."
            return
        self.run_command(["./bin/fu-mission", command], cwd=run["path"])

    def handle_key(self, key):
        if key in (ord("q"), 27):
            return False
        if key in (ord("1"), ord("2"), ord("3"), ord("4")):
            self.tab = int(chr(key)) - 1
        elif key in (ord("r"),):
            self.message = "Refreshing..."
            self.refresh()
            self.message = "Refreshed."
        elif key == ord("d"):
            self.run_default()
        elif key in (curses.KEY_UP, ord("k")):
            if self.tab == 1:
                self.selected_mission = max(0, self.selected_mission - 1)
            elif self.tab == 2:
                self.selected_action = max(0, self.selected_action - 1)
            elif self.tab == 3:
                self.selected_contribution = max(0, self.selected_contribution - 1)
        elif key in (curses.KEY_DOWN, ord("j")):
            if self.tab == 1:
                self.selected_mission = min(len(self.missions) - 1, self.selected_mission + 1)
            elif self.tab == 2:
                self.selected_action = min(len(self.active_actions()) - 1, self.selected_action + 1)
            elif self.tab == 3:
                self.selected_contribution = min(len(self.contributions) - 1, self.selected_contribution + 1)
        elif key in (curses.KEY_ENTER, 10, 13, ord("s")):
            self.open_or_run()
        elif key == ord("c"):
            self.claim_action()
        elif key == ord("o"):
            self.workspace_command("room")
        elif key == ord("f"):
            self.workspace_command("finish")
        elif key == ord("p"):
            self.workspace_command("experiment-preview")
        elif key == ord("P"):
            self.workspace_command("experiment-publish")
        return True

    def loop(self):
        curses.curs_set(0)
        self.screen.nodelay(False)
        while True:
            self.draw()
            key = self.screen.getch()
            if not self.handle_key(key):
                break


def main(screen, base_url, runner_path):
    if curses.has_colors():
        curses.start_color()
        curses.use_default_colors()
        screen.bkgd(" ", curses.color_pair(0))
    ControlRoom(screen, base_url, runner_path).loop()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Future Union native Control Room")
    parser.add_argument("--base", default=os.environ.get("FU_BASE_URL", "https://www.futureunion.org.uk"))
    parser.add_argument("--runner", default=os.environ.get("FU_RUNNER_PATH", "./fu-runner.sh"))
    args = parser.parse_args()
    curses.wrapper(main, args.base, args.runner)
