import argparse
import faulthandler
import os
import stat
import time
from dataclasses import dataclass
from pathlib import Path
from controllers.types import Agents, CONTROLLERS
import features
from commands.launch import launch
from engine import attic
from engine.record import Record
from resources.base import PROJECT
from surfaces.everything import found
from engine.seats import seats
from engine.transcript import Turn, search as search_transcript
from features.command_tags.reading import visible
from providers import PROVIDERS
from providers.turns import LoadLimit, every_turn, loading
from resources.base import Refused, SYSTEM
from resources.types import AgentRow
from engine import runtime
from engine.stored import last_lines

SLOW_LOG = "ended-slow.log"
SLOW_AFTER = 10


def transcript(record, session: str):
    if not session:
        return []
    row = Agents(record, actor=SYSTEM).by_session(session)
    provider = PROVIDERS.get(row.provider)
    return provider().turns(Path(row.transcript)) if provider else []

def turn_text(turn, source: str = "") -> str:
    return f"{source}{turn.line:>6}  {turn.who:<7} {visible(turn.text)}"

def say(turns) -> str:
    return "\n".join(turn_text(turn) for turn in turns)

@dataclass(frozen=True)
class SourcedTurn:
    provider: str
    session: str
    turn: Turn

    @property
    def text(self) -> str:
        return self.turn.text

    @property
    def lowered(self) -> str:
        return self.turn.lowered

MERGED: dict = {}
LOAD_LIMIT = 0.4


def environment_transcript(record, limit: LoadLimit | None = None) -> list[SourcedTurn]:
    """Every turn of the environment's agents' conversations in order, each conversation once: its file is found by one stat, the same file by its device and inode; the merge is kept while no conversation has changed."""
    seen = set()
    sources = []
    for row in reversed(Agents(record, actor=SYSTEM).rows.every()):
        provider = PROVIDERS.get(row.provider)
        try:
            found = os.stat(Path(row.transcript).expanduser()) if row.transcript else None
        except (OSError, RuntimeError):
            continue
        if not provider or not found or not stat.S_ISREG(found.st_mode) or (found.st_dev, found.st_ino) in seen:
            continue
        seen.add((found.st_dev, found.st_ino))
        sources.append((row, every_turn(row, limit)))
    kept = MERGED.get((record.root, record.env))
    if kept and len(kept[0]) == len(sources) and all((row.n, row.title) == named and here is turns for (row, here), (named, turns) in zip(sources, kept[0])):
        return kept[1]
    turns = [(turn.at, row.n, turn.line, SourcedTurn(row.provider, row.title, turn)) for row, here in sources for turn in here]
    turns.sort(key=lambda item: item[:3])
    merged = [item[-1] for item in turns]
    MERGED[record.root, record.env] = ([((row.n, row.title), here) for row, here in sources], merged)
    return merged


def attic_text(record, term: str) -> str:
    hits = attic.searched(record.root, term)
    if not hits:
        return f"nothing in the removed environments mentions {term!r}"
    lines = "\n".join(f"  {hit.environment}  {hit.ref.replace(':', ' ')}  {hit.title}" for hit in hits)
    return f"{lines}\nbring one back with journal environment unarchive <name>"


def search_text(record, term: str, page: int, archived: bool = False, resources: str = "") -> str:
    limit = LoadLimit.after(LOAD_LIMIT)
    transcript_matches = "\n".join(turn_text(hit.turn, f"{hit.provider}:{hit.session}  ")
                                   for hit in search_transcript(environment_transcript(record, limit), term, page))
    hits = found(record, SYSTEM, term, archived, tuple(filter(None, resources.split(","))))
    types = list(dict.fromkeys(hit.type for hit in hits))
    rows = "\n".join(f"{type_} ({len(here)})\n" + "\n".join(row_line(hit) for hit in here)
                     for type_ in types if (here := [hit for hit in hits if hit.type == type_]))
    files = "\n".join(f"  file  {hit.row.ref}  {name}" + (f" — {tags}" if tags else "") for hit in hits for name, tags in hit.files)
    unread = max(loading(), len(limit.left))
    still = f"{unread} conversations are still being read, so older matches may be missing: search again in a moment" if unread else ""
    return "\n".join(part for part in (transcript_matches, rows, files, still) if part)


def row_line(hit) -> str:
    gone = f"  (archived, bring back with journal {hit.type} restore {hit.row.n})" if hit.row.deleted else ""
    return f"  {hit.row.ref}  {hit.row.title}{gone}"


def switched(ctx, on: bool) -> str:
    if on:
        runtime.OFF.lower_flag(ctx["record"].root)
        return "the journal is in force"
    runtime.OFF.raise_flag(ctx["record"].root)
    return "the journal is off: the hooks report nothing and hold nothing until journal enable"

def verify(ctx) -> str:
    root = ctx["record"].root
    lines = [f"root {root}", f"environment {ctx['record'].env}", "off" if runtime.off(root) else "in force"]
    for name, provider in PROVIDERS.items():
        settings = provider().config(root.parent)
        wired = settings.is_file() and "hook.sh" in settings.read_text()
        lines.append(f"{name}: hooks {'wired' if wired else 'NOT wired'} ({settings})")
    live = seats(root, within=10)
    lines.append(f"engine: {len(live)} running — {', '.join(seat.terminal[:8] for seat in live) or 'none'}")
    row = Agents(ctx["record"], actor=ctx["actor"]).by_session(ctx["session"]) if ctx["session"] else None
    if row:
        lines.append(f"this session: {row.status or '?'} after {row.event or '?'}, {row.uses} tool uses")
    return "\n".join(lines)

def settings_text(ctx) -> str:
    record = ctx["record"]
    out = [f"settings on {record.env} ({record.home / 'settings.json'}); the project's own are in {record.root / 'settings.json'}"]
    for name, f in features.FEATURES.items():
        out.append(f"  features.{name:<14} {'on' if f.enabled(record) else 'off'}   {f.trigger or ''}{'   whole project' if f.scope == PROJECT else ''}")
    for key in Record.SETTINGS:
        if key != Record.features and record.setting(key):
            out.append(f"  {key}: {record.setting(key)}")
    out.append("  change one: journal settings are written by the viewer's Settings page, or POST /api/<env>/settings")
    return "\n".join(out)

def help_text(word: str) -> str:
    from commands.parser import parser
    p = parser()
    if not word:
        return p.format_help()
    for action in p._actions:
        if isinstance(action, argparse._SubParsersAction) and word in action.choices:
            return action.choices[word].format_help()
    return f"no command {word!r}"

def speed(ctx) -> str:
    from commands.speed import measure
    if ctx["board"]:
        from commands.speed_board import measure_board
        return measure_board(ctx["runs"], Path(ctx["out"]) if ctx["out"] else None)
    return measure(ctx["record"].root, ctx["record"].env, ctx["runs"], ctx["url"], ctx["out"])

def demo(ctx) -> str:
    from commands.demo import demo_built
    return demo_built(Path(ctx["folder"]), Path(ctx["into"]), ctx["environment"], ctx["name"])

def upgrade_here(ctx) -> str:
    from install import upgrade
    root = ctx["record"].root
    return "\n".join(upgrade(root.parent, root, yes=ctx["yes"], version=ctx["to"]))

def halt(ctx) -> str:
    from engine.stop import ask, clear, ended
    record = ctx["record"]
    root = record.root
    left = still_open(record)
    from features.helpers.controller import stop_helpers
    stop_helpers(record)
    ask(root)
    from features.clean_slate.slate import put_back
    put_back(record)
    went = ended(root)
    clear(root)
    summary = "the journal is stopped: its viewer, its engine and every service it ran" if went else "the server did not stop, even when told to end; see .journal/runtime/viewer.log"
    return "\n".join([summary, *left])


def still_open(record) -> list[str]:
    from controllers.types import Works
    from features import FEATURES
    from features.messages.answering import unanswered
    works = [f"work {w.n} is still open: {w.title} - journal work end {w.n} --how \"<what landed>\", or journal work park \"<why>\" --n {w.n}"
             for w in Works(record, actor=SYSTEM).rows.standing()]
    messages = [f"message {m.n} was read and never answered: {m.title}" for m in unanswered(FEATURES["messages"].journal.at(record))]
    return works + messages

def attached(ctx) -> str:
    from agents.screen import attach
    return attach(ctx["record"].root, ctx["target"])


def supervise(ctx, agent: str) -> str:
    return launch(ctx["record"], agent, ctx["args"])


def ended(ctx) -> str:
    from engine.sessions import Sessions
    from features.clean_slate.slate import put_back
    from engine.stop import ask, stays_up
    from engine.typist import live
    root = ctx["record"].root
    folder = runtime.folder(root)
    folder.mkdir(parents=True, exist_ok=True)
    with open(folder / SLOW_LOG, "a") as trace:
        faulthandler.dump_traceback_later(SLOW_AFTER, exit=False, file=trace)
        try:
            put_back(ctx["record"])
            kept_work(Path.cwd())
            if not live(root) and not Sessions(root).running() and not stays_up(ctx["record"]):
                ask(root)
        finally:
            faulthandler.cancel_dump_traceback_later()
    if (folder / SLOW_LOG).stat().st_size == 0:
        (folder / SLOW_LOG).unlink()
    return ""


def kept_work(cwd: Path) -> None:
    from engine.worktree import checkout, git, keep, main_checkout, repositories
    from providers import workspace_folders
    top = checkout(cwd, workspace_folders())
    if not top:
        return
    for place in [top] if (top / ".git").exists() else repositories(top):
        keep(main_checkout(place), top.name, git(place, "branch", "--show-current").stdout.strip())


def healed(ctx) -> str:
    from engine.heal import heal, healed_after_death
    return (healed_after_death if ctx["died"] else heal)(ctx["record"].root)

def services(ctx) -> str:
    from engine.services import DOWN, UP, Declared, listed, log_file, specs, want
    from features.plugins.services import plugin_services
    root = ctx["record"].root
    action, which = ctx["action"], ctx["which"]
    if action == "up":
        return services_up(root)
    if action == "log":
        return tail(log_file(root, which), ctx["lines"])
    if action in ("start", "stop", "restart"):
        if not which:
            raise Refused(f"say which service to {action}: journal services {action} <plugin>.<service>")
        want(root, which, DOWN if action == "stop" else UP, nonce=time.time() if action == "restart" else 0.0)
        return f"{which} is asked to {'stop' if action == 'stop' else 'run'}"
    if action != "list":
        raise Refused(f"services knows list, up, start, stop, restart and log, not {action!r}")
    lines = [f"{s['id']:<28} {s['state']:<10} {s['url']}{'  ' + s['why'] if s['why'] else ''}" for s in listed(root, [Declared(one.id, one.plugin, one.service, one.port) for one in specs(root, (plugin_services,))])]
    return "\n".join(lines) or "no plugin declares a service"

def services_up(root: Path) -> str:
    from engine.services import Manager
    from features.plugins.services import plugin_services
    from engine.services import lifeline
    alive, keeping = lifeline()
    manager = Manager(root, alive, sources=(plugin_services,))
    print("journal: keeping the plugins' services up; Ctrl-C stops them")
    try:
        while True:
            manager.tick()
            time.sleep(1.0)
    except KeyboardInterrupt:
        pass
    finally:
        os.close(keeping)
    return "the services are stopped"

def tail(path: Path, lines: int) -> str:
    return last_lines(path, lines) if path.is_file() else f"nothing is logged in {path}"

def serve_forever(ctx) -> str:
    from serve import run
    from engine.viewer import SERVED_ON, free_from
    run(ctx["record"].root, ctx["port"] or free_from(SERVED_ON))
    return ""

def decided(ctx) -> str:
    agents = Agents(ctx["record"], actor=SYSTEM)
    named = agents.rows.by_title(ctx["session"]) if ctx["session"] else None
    row = named if named is not None and named.at else agents.primary()
    if row is None:
        raise Refused("no agent session is running on this environment to note it on")
    agents.update(row.n, **{**row.data, AgentRow.decided: ctx["why"]})
    return f"noted: {ctx['why']} - the checkpoint at {int(row.context)}% is released"
