#!/usr/bin/env python3.10

import typer
from rich import print
from rich.markup import escape
from rich.console import Console

from typer import Abort
from enum import Enum
from exploitfarm.utils.reqs import get_url
from exploitfarm.cmd.config import InitialConfiguration, inital_config_setup, ClientConfig
from exploitfarm.cmd.login import login_required, try_authenticate
from exploitfarm.cmd.exploitinit import ExploitConf
from exploitfarm.utils.config import ExploitConfig, check_exploit_config_exists
import getpass, re, os, orjson, time
from pydantic import PositiveInt
from typing import Optional
from uuid import UUID
from exploitfarm.model import Language
from exploitfarm.utils.config import EXPLOIT_CONFIG_REGEX
from multiprocessing import Manager
from exploitfarm.utils import restart_program
from exploitfarm.cmd.startxploit import start_exploit_tui
from exploitfarm.utils.reqs import ReqsError
from requests.exceptions import Timeout as RequestsTimeout

import traceback

app = typer.Typer(
    no_args_is_help=True,
    add_completion=False
)
console = Console()

class g:
    interactive = True
    config: ClientConfig = ClientConfig.read()

def initial_setup(login=True):
    connection = inital_config_setup(g.config, interactive=g.interactive)
    if login and connection:
        login_required(g.config, interactive=g.interactive)
    return connection

@app.command(help="Configure the client settings")
def config(
    address: str = typer.Option(None, help="The address of the server"),
    port: int = typer.Option(None, help="The port of the server"),
    nickname: str = typer.Option(None, help="The nickname of this client"),
    https: bool = typer.Option(False, help="Use HTTPS for the connection")
):
    if g.interactive:
        init_config = InitialConfiguration(g.config)
        if init_config.run() == 0:
            print("[bold green]Configuration saved![/]")
        else:
            print("[bold red]Configuration cancelled[/]")
    else:
        if address:
            g.config.server.address = address
        if port:
            g.config.server.port = port
        if nickname:
            g.config.client_name = nickname
        if https:
            g.config.server.https = https
        elif not g.config.test_server():
            print(f"[bold red]Connection test failed to {escape(get_url('//', g.config))}[/]")
            return
        g.config.write()
        print("[bold green]Config updated[/]")

@app.command(help="Reset the client settings")
def reset():
    print("[bold yellow]Are you sure you want to reset configs?\n[bold red]This operation may break some exploits running on the client.", end="")
    delete = typer.confirm("")
    if delete:
        ClientConfig().write()
        print("[bold green]Client resetted successful[/]")
    else:
        print("[bold]Reset cancelled[/]")

@app.command(help="Start the exploit")
def start(
    path: str = typer.Argument(".", help="The path of the exploit"),
    pool_size: PositiveInt = typer.Option(50, "--pool-size", "-p", help="The number of workers to start"),
    submit_pool_timeout: PositiveInt = typer.Option(3, help="The timeout for the submit pool to wait for new attack results and send flags"),
    server_status_refresh_period: PositiveInt = typer.Option(5, help="The period to refresh the server status"),
    test: Optional[str] = typer.Option(None, "--test", "-t", help="Test the exploit"),
    test_timeout: PositiveInt = typer.Option(30, help="The timeout for the test"),
):
    path = os.path.abspath(path)
    from exploitfarm.xploit import start_xploit, shutdown, xploit_one
    
    if not os.path.isdir(path):
        print(f"[bold red]Path {escape(path)} not found[/]")
        return
    
    if not check_exploit_config_exists(path):
        print(f"[bold red]Exploit configuration not found in {escape(path)}[/]")
        return
    
    if test:
        xploit_one(g.config, test, path, test_timeout)
        return

    if not initial_setup():
        print("[bold red]Can't connect to the server! The server is needed to start the exploit! Configure with 'xfarm config'[/]")
        return
    
    try:
        exploit_config = ExploitConfig.read(path)
        if exploit_config.service not in [UUID(ele["id"]) for ele in g.config.status["services"]]:
            print(f"[bold red]Service {escape(str(exploit_config.service))} not found use 'xfarm init --edit'[/]")
            decision = typer.confirm("Do you want to continue run 'xfarm init --edit' ?", default=True)
            if decision:
                init(edit=True)
                restart_program()
            return
        exploit_config.publish_exploit(g.config)
    except Exception as e:
        traceback.print_exc()
        print(f"[bold red]Error reading exploit configuration from {path}: {e}[/]")
        return

    mem_manager = Manager()
    shared_infos = mem_manager.dict()
    print_queue = mem_manager.Queue(100)
    shared_infos["config"] = g.config.status
    if not exploit_config.lock_exploit():
        print("[bold yellow]⚠️ Exploit is already running, do you want to continue? (This process will not be tracked)[/]", end="")
        cont = typer.confirm("", default=False)
        if not cont:
            print("[bold red]Operation cancelled[/]")
            return
    exit_event = mem_manager.Event()
    restart_event = mem_manager.Event()
    start_xploit(g.config, shared_infos, print_queue, pool_size, path, submit_pool_timeout, server_status_refresh_period, exit_event, restart_event)
    if g.interactive:
        start_exploit_tui(shared_infos, exploit_config, print_queue, pool_size, exit_event, restart_event)
        shutdown()
    else:
        try:
            while True:
                print(print_queue.get())
        except KeyboardInterrupt:
            print("[bold yellow]Shutting down the exploit[/]")
            shutdown()
    if restart_event.is_set():
        print("[bold yellow]Restarting the exploit[/]")
        restart_program()
        

@app.command(help="Login to the server")
def login(
    password: str = typer.Option(None, help="The password of the user"),
    stdin: bool = typer.Option(False, help="Read the password from stdin"),
):
    initial_setup(login=False)
    
    if g.config.status["status"] == "setup":
        print("[bold red]Please configure the server first[/]")
        return
    if g.config.status["loggined"] and not g.config.status["config"]["AUTHENTICATION_REQUIRED"]:
        print("[bold green]Authentication is not required[/]")
        return
    if g.config.status["loggined"]:
        print("[bold green]Already logged in![/]")
        return
    
    if stdin or (not password and not g.interactive):
        if g.interactive:
            password = getpass.getpass("Password: ")
        else:
            password = input("Password: ")
        status, error = try_authenticate(password, g.config)
        if status:
            print("[bold green]Logged in![/]")
        else:
            print(f"[bold red]Error: {escape(error)}[/]")
        return

    if password:
        status, error = try_authenticate(password, g.config)
        if status:
            print("[bold green]Logged in![/]")
        else:
            print(f"[bold red]Error: {escape(error)}[/]")
        return

    login_required(g.config, interactive=g.interactive)

@app.command(help="Logout from the server")
def logout():
    g.config.server.auth_key = None
    g.config.write()
    print("[bold red]Logged out[/]")

@app.command(help="Test a submitter")
def submitter_test(
    path: str = typer.Argument(help="Submitter python script"),
    kwargs: str = typer.Option("{}", help="Submitter key-words args (json)"),
    output: str = typer.Argument(help="Text containing flags according to server REGEX")
):
    initial_setup()
    try:
        kwargs = orjson.loads(kwargs)
    except Exception as e:
        print(f"[bold red]Invalid kwargs json: {e}")
        return
    
    try:
        with open(path, "rt") as f:
            submitter_code = f.read()
    except Exception as e:
        print(f"[bold red]File {escape(path)} not found: {e}")
        return
    
    if not output:
        print("[bold red]Output can't be empty")
    
    flags = [output]
    if g.config.status["config"]["FLAG_REGEX"]:
        flags = re.findall(g.config.status["config"]["FLAG_REGEX"], output)
    
    if len(flags) == 0:
        print(f"[bold red]No flags extracted from output! REGEX: {escape(g.config.status['config']['FLAG_REGEX'])}")
        return
    submitter_id = None
    try:
        submitter_id:int = g.config.reqs.new_submitter({
            "name": "TEST_SUBMITTER (Will be deleted soon)",
            "kargs": kwargs,
            "code": submitter_code
        })["id"]
        print("[bold yellow]----- TEST RESULTS -----")
        print("[bold yellow]Flags to submit:[/]", flags)
        print("[bold yellow]Output:[/]")
        print(g.config.reqs.test_submitter(submitter_id, flags))
        print("[bold yellow]----- TEST RESULTS -----")
    finally:
        if submitter_id:
            g.config.reqs.delete_submitter(submitter_id)

class StatusWhat(Enum):
    status = "status"
    submiters = "submitters"
    services = "services"
    exploits = "exploits"
    flags = "flags"
    teams = "teams"
    clients = "clients"

@app.command(help="Get status of the server")
def status(
    what:StatusWhat = typer.Argument(StatusWhat.status.value, help="Server informations type")
):
    initial_setup()
    match what:
        case StatusWhat.status:
            print(g.config.status)
        case StatusWhat.submiters:
            print(g.config.reqs.submitters())
        case StatusWhat.services:
            print(g.config.reqs.services())
        case StatusWhat.exploits:
            print(g.config.reqs.exploits())
        case StatusWhat.flags:
            print(g.config.reqs.flags())
        case StatusWhat.teams:
            print(g.config.reqs.teams())
        case StatusWhat.clients:
            print(g.config.reqs.clients())

@app.command(help="Initiate a new exploit configuration")
def init(
    edit: bool = typer.Option(False, "--edit", "-e", help="Edit the exploit configuration"),
    name: Optional[str] = typer.Option(None, help="The name of the exploit"),
    service: Optional[UUID] = typer.Option(None, help="The service of the exploit"),
    language: Optional[Language] = typer.Option(None, help="The language of the exploit"),
):
    initial_setup()
    if g.interactive:
        if edit:
            if check_exploit_config_exists("."):
                expl_conf = ExploitConfig.read(".")
                name = expl_conf.name
                service = expl_conf.service
                language = expl_conf.language
            else:
                print("[bold red]Exploit configuration not found![/]")
                return
            init_config = ExploitConf(g.config, edit, name, service, language)
        else:
            init_config = ExploitConf(g.config, edit)
        final_status = init_config.run()
        if final_status == 0:
            print(f"[bold green]Exploit configuration {'created' if not edit else 'edited'}![/]")
        elif final_status == 99:
            print("[bold yellow]Exploit folder created, but not registered on the server![/]")
        else:
            print("[bold red]Exploit configuration cancelled[/]")
    else:
        exists = check_exploit_config_exists(name if not edit else ".")
        
        if edit ^ exists:
            print(f"[bold red]Exploit '{escape(name)}' already exists!")
            return
        
        if (not name or edit) or not re.match(EXPLOIT_CONFIG_REGEX, name):
            print(f"[bold red]Please provide a valid name for the exploit (regex: {escape(EXPLOIT_CONFIG_REGEX)})[/]")
            return
        
        try:
            if not service in [UUID(ele["id"]) for ele in g.config.status["services"]]:
                service = None
                if not edit:
                    print("[bold red]Service not found, add a new on the server[/]")
                    return
        except Exception:
            print("[bold red]Service id not found[/]")
            return
        
        if (not language or edit):
            print("[bold red]Language not found[/]")
            return

        if edit:
            expl_conf = ExploitConfig.read(name)
            if name: expl_conf.name = name
            if language: expl_conf.language = language
            if service: expl_conf.service = service
        else:
            expl_conf = ExploitConfig.new(name, language, service)
        expl_conf.write(name)
        expl_conf.publish_exploit(g.config)
        if edit:
            print("[bold green]Exploit configuration updated![/]")
        else:
            print("[bold green]Exploit configuration created![/]")


@app.callback()
def main(no_interactive: bool = typer.Option(False, "--no-interactive", "-I", help="Interactive configuration mode", envvar="XFARM_INTERACTIVE")):
    g.interactive = not no_interactive

if __name__ == "__main__":
    try:
        app()
    except KeyboardInterrupt:
        print("[bold yellow]Operation cancelled[/]")
    except Abort:
        print("[bold yellow]Operation cancelled[/]")
    except ReqsError as e:
        print("[bold red]The server returned an error: {e}[/]")
    except RequestsTimeout as e:
        print(f"[bold red]The server has timed out: {e}[/]")
