#!/usr/bin/env python3

import sys
from pathlib import Path
from bg_cliBuilder import CliBuilder, opt, pos, bgtrace, bgnormstr, ExpectedException


# These are mockup completers. In the real bg-agent command they make calls into
# the Agent class to get the various domain specific data
# These mockup try to demonstrate the common features

def bcGetAgentClasses():
    return ["Jack","Jill","Sid"]

# agentClass can be an argument to the completer fn because when ever a sessionID
# is being entered, the sub command also allows --agentClass or <agentClass> to be
# entered. This means that the "agentClass" varName will always exist in the parsed
# namespace but it might be the default (None) value if the user has not included
# it on the command line. The completer was used in some places that allowed the
# desired varName and some places that it does not, we could pass argsNS instead
# and test hasattrib(argsNS, "<varName>")
def getSessionIds(agentClass=None):
    bgtrace(f"agentClass={agentClass!r}")
    # in this example if agentClass exists we return only the agent sessions of that
    # class. Otherwise we return all known sessions. Of course in the real command
    # the suggestions are not hard coded
    if agentClass:
        return [agentClass.lower()]
    else:
        # completer functions can return a space separated list of suggestions
        return ["jack1","jack2","jill","sid"]

def completeNewSessionIds(agentClass):
    existingSessions = getSessionIds()
    print(f"<existing:{','.join(existingSessions)}>")

def getToolNames():
    # completer functions can return a list of suggestions. This is particular useful
    # when the suggested words might contain spaces which are automatically escaped
    # because we know each list element should be one suggestion.
    return ["readFile","tool Two"]

def getToolArgs(toolName):
    # tool args are json formated strings. We dont try to complete inside the string
    # but at least we can let the user know what named arguments the tool accepts
    match toolName:
        case "readFile":
            return "<filePath=> <[maxLines=]>"
        case "toolTwo":
            return "<toolArgA=> <toolArgB=>"
    return ""

def completeModelIDs():
    # in the real bg-agent Agent.getModelIDs() is async so we cache them in a file
    # and populate the cache asynchronuously when the cache is stale
    if Path("cachFile").exists():
        return Path("cachFile").read_text()
    else:
        # send a message that the suggestions are being retrieved so the user knows to
        # try again to see the suggestions
        print("<pending%20retrieval...>")

        # normally the BGBC driver caches completions so it does not invoke the command again
        # until the user modifies the command line being entered. But in this case we want it
        # to keep calling us to see when the suggestions are ready.
        # This BGBC directive instructs it not to cache this set of suggestions.
        print("$(nocache)")

        # the command to retireve the modelIDs and populate the cache is written
        # so that it is a noop if a retrieval is in flight
        #launchBackgroundTaskToPopulateCacheFile

async def main(argv=None) -> int:

    #---------------------------------------------------------------------------
    # Declare the command line syntax

    cli = CliBuilder(prog="bg-agent", helpNotation=helpNotation)
    cli.enableVerbosityFeature()
    cli.enablePwdFeature()

    cli.cmds("""
        --agentClass:str=
        init
        sessions    [list]
        sessions    create <agentClass> <newSessionID>
        classes     [list]
        classes     copy <agentClass> <newClassName>
        upstreams   [list] --agentClass:str
        models      [list] --agentClass:str
        apps        [list] --agentClass:str
        <sessionID> reset
        <sessionID> undo <n>:int=1
        <sessionID> tools [list]
        <sessionID> tools call <toolName> <argsJsonStr>
        <sessionID> info
        <sessionID> journal
        <sessionID> context <intrSpec>:int=-1
        <sessionID> apps <intrSpec>:int=-1
        <sessionID> data <intrSpec>:int=-1
        <sessionID> convo <intrSpec>:int=-1
        <sessionID> run --reset --modelID=<id> <instructions>:...
        """)

    cli.addCompleter("<agentClass>",  bcGetAgentClasses)
    cli.addCompleter("--agentClass",  bcGetAgentClasses)
    cli.addCompleter("<sessionID>",   getSessionIds)
    cli.addCompleter("<newSessionID>",completeNewSessionIds)
    cli.addCompleter("<toolName>",    getToolNames)
    cli.addCompleter("<argsJsonStr>", getToolArgs)
    cli.addCompleter("--modelID",     completeModelIDs)

    cli.parseArgs()

    #---------------------------------------------------------------------------
    # Main script

    # we need a global verbosity so that it can affect the exception handler
    global verbosity
    verbosity = cli.verbosity


    cli.printNS()
    print("")

    print(f"working dir = '{Path.cwd()}'")
    print("")

    match cli.rootCmd:
        case "init":
            print("DISPATCH: agentInit()")

        case "sessions":
            match cli.sessionsCmd or "list":
                case "list":
                    print("DISPATCH: sessionsList()")
                case "create":
                    if cli.newSessionID in getSessionIds():
                        raise ExpectedException(bgnormstr(f"""
                            session create: Bad newsessionID. Name already taken.
                                newSessionID : {cli.newSessionID}
                                existing names : {bgnormstr(','.join(getSessionIds()), indent=36, first="")}
                            """))
                    print(f"DISPATCH: sessionsCreate(agentClass={cli.agentClass!r}, sessionID={cli.newSessionID!r})")
                case _:
                    cli.error("invalid sessions command")

        case "classes":
            match cli.classesCmd or "list":
                case "list":
                    print("DISPATCH: classesList()")
                case "copy":
                    print(f"DISPATCH: classesList(agentClass={cli.agentClass!r}, newClassName={cli.newClassName})")
                case _:
                    cli.error("invalid classes command")

        case "upstreams":
            match cli.upstreamsCmd or "list":
                case "list":
                    print(f"DISPATCH: upstreamsList(agentClass={cli.agentClass!r})")
                case _:
                    cli.error("invalid upstreams command")

        case "models":
            match cli.modelsCmd or "list":
                case "list":
                    print(f"DISPATCH: modelsList(agentClass={cli.agentClass!r})")
                case _:
                    cli.error("invalid models command")

        case "apps":
            match cli.appsCmd or "list":
                case "list":
                    print(f"DISPATCH: appsList(agentClass={cli.agentClass!r})")
                case _:
                    cli.error("invalid apps command")

        case "captured":
            print(f"DISPATCH: capturedView(idx={cli.idx!r})")

        case "<sessionID>":
            match cli.sessionIDCmd:
                case "run":
                    print(
                        "DISPATCH: sessionRun("
                        f"sessionID={cli.sessionID!r}, "
                        f"modelID={cli.modelID!r}, "
                        f"instructions={' '.join(cli.instructions)!r})"
                    )

                case "reset":
                    print(f"DISPATCH: sessionReset(sessionID={cli.sessionID!r})")

                case "undo":
                    print(f"DISPATCH: sessionUndo(sessionID={cli.sessionID!r}, n={cli.n!r})")

                case "tools":
                    match cli.toolsCmd or "list":
                        case "list":
                            print(f"DISPATCH: sessionToolsList(sessionID={cli.sessionID!r})")
                        case "call":
                            print(
                                "DISPATCH: sessionToolCall("
                                f"sessionID={cli.sessionID!r}, "
                                f"toolName={cli.toolName!r}, "
                                f"argsJsonStr={cli.argsJsonStr!r})"
                            )
                        case _:
                            cli.error("invalid tools command")

                case "info":
                    print(f"DISPATCH: sessionInfo(sessionID={cli.sessionID!r})")

                case "journal":
                    print(f"DISPATCH: sessionJournal(sessionID={cli.sessionID!r})")

                case "context":
                    print(f"DISPATCH: sessionContext(sessionID={cli.sessionID!r}, interactionSpec={cli.interactionSpec!r})")

                case "apps":
                    print(f"DISPATCH: sessionApps(sessionID={cli.sessionID!r}, interactionSpec={cli.interactionSpec!r})")

                case "data":
                    print(f"DISPATCH: sessionData(sessionID={cli.sessionID!r}, interactionSpec={cli.interactionSpec!r})")

                case "convo":
                    print(f"DISPATCH: sessionConvo(sessionID={cli.sessionID!r}, interactionSpec={cli.interactionSpec!r})")

                case _:
                    cli.error("invalid or incomplete session command")

        case _:
            cli.error(f"invalid command '{cli.cmd}'")

    return 0


helpNotation="""
Run agents in the context of a project folder.

Agent Classes:
An agent class defines the behavior of an agent instance. They are defined in the scoped
configuration folder(s) under the AgentClasses/ sub folder. They can specify the following...
- the upstream LLM models that the agent can use
- static system prompts that appear at the start of the context
- the set of LLMApps that the agent can access.
- they can override the instruction prompts from LLMApps
- LLM completion parameters use for upstream requests

Agent Instances:
To run an agent you first create a new named instnace of an agent class. An instance
starts with a context populated with the starting state that the class defines. Then
with each interaction the context converstaion grows and dynamic areas of the context
can change.

You can inspect the current and previous states of the agent instance's context with
various <sessionID> sub commands.

Agent Interaction:
An agent interaction starts with a role=user prompt being sent to the agent and the
subsequent upstream LLM turns that the agent makes to perform the action specified in
the prompt.

You can invoke an agent interaction with <sessionID> run <instruction> sub command.

Agent Turn:
An agent turn is one upstream LLM completion request and response and any tool calls
specified in the LLM's response. The context conversation consists of turns nested inside
interactions.

You can inspect the state of the context at any turn to see exactly what the LLM saw.

LLMApps:
LLMApps are applications that the agent can use to interact with the computer and outside
resources.

[cmd:apps]
   LLMApps are applictions that the agent can use to do things
[cmd:classes]
   Agent classes define the behavior of agent instances
[cmd:init]
   Initialize a project folder so that agents can run in it
[cmd:models]
   Models are upstream LLMs that agents can use
[cmd:upstreams]
   Upstreams are the remote servers that provide models
[cmd:sessions]
   Sessions are agent class instances that have a context
[cmd:sessions list]
   List the agent instances that exist in this project folder
[cmd:sessions create]
   Create a new agent instance with a given name
[cmd:<sessionID>]
   A sessionID is the name of a ongoing agent instance
[cmd:<sessionID> reset]
   Return the agent's context to its starting state
[cmd:<sessionID> undo]
   Remove the last <n> interactions from the agent's context
[cmd:<sessionID> tools list]
   List the tools that are visible to the agent at the current state or at some point in the conversation
[cmd:<sessionID> tools call]
   Make a tool call to affect the context as if the agent had called it
[cmd:<sessionID> info]
   View some meta data about the agent instance.
[cmd:<sessionID> journal]
   The journal is a json structured history of the conversation.
[cmd:<sessionID> context]
   View a human readable rendering of the context that the agent sees.
[cmd:<sessionID> apps]
   View the LLMApp instructions that the agent sees.
[cmd:<sessionID> data]
   View the dynamic reference data section of the context.
[cmd:<sessionID> convo]
   View just the conversation part of the context. This excludes the static and dynamic areas
   at the start of the context.
[cmd:<sessionID> run]
   Run an agent interaction. This prompts the agent and the agent will run one or more
   turns until it stops.


[arg:<agentClass>]
   The agent class defines the behavior of the agent. See classes sub command.
[arg:<sessionID>]
   This is the name of an agent instance.
[arg:<intrSpec>]
   Specifies a point in the session's history to inspect
   <i>[:<t>] -> A specific interaction and turn (clipped to those available) Where
      <i> is i'th interaction in the context. Starts at 1. Negative numbers are relative to the last.
      <t> is t'th turn within an interaction. Starts at 1. Negative numbers are relative to the last.
[arg:<toolName>]
   The name of the tool to be invoked
[arg:<argsJsonStr>]
   A string containing json text of the arguments passed to the tool call
[arg:<n>]
   the number of interactions to undo
[arg:<instructions>]
   The text sent as the prompt to start an agent interaction.

[opt:--pwd]
   run the command with <path> as the current directory
[opt:-v]
   increase verbosity (can be multiple -vvv or -v -v)
[opt:-q]
   decrease verbosity (can be multiple -qqq or -q -q)
[opt:--agentClass]
   For upstreams,models,apps commands. Limit the scope to those available in the agentClass <c>.
[opt:--modelID]
   the upstream model for the agent to use in the interaction. It is sticky so it will become the default until it is changed
[opt:--reset]
   Reset the context before running the interaction.

"""


if __name__ == "__main__":
    CliBuilder.run(main)
