"""Remote deployment of hawk-soard to Windows target hosts via WinRM/PowerShell.

Provides script-building helpers called by HybridTaskExecutor._deploy_hawk_soard().
The deploying hawk-soard node uses PowerShell Invoke-Command (WinRM) to push
hawk-soard onto the destination host, configure it, and start it as a Windows service.

Two deployment strategies are supported:

  Push (preferred)
    When a local NSIS installer is found on disk (see ``find_local_installer``),
    it is copied to the remote host via a PSSession + ``Copy-Item -ToSession``
    and then run silently.  Credentials are forwarded to the NSIS installer
    via ``/ACCESS_TOKEN=`` and ``/SECRET_KEY=`` on the command line AND the
    config is written again via PowerShell afterwards to guarantee the correct
    values even on re-deployment (the NSIS installer skips config-writing when
    a previous config file already exists).

  Download
    When no local installer is available the remote host downloads the package
    from ``source_url``.  ``.exe`` packages receive the same NSIS CLI treatment.
    ``.zip`` packages fall back to manual config-write and service registration.

In both strategies the config is always (re-)written via PowerShell and the
service is always restarted so the new credentials take effect immediately.

Config flag required in hawk-soard.cfg to enable this action:
    enable_remote_deploy = true

Optional allowlist (CSV of hostnames/IPs):
    remote_deploy_allowed_targets = host1,host2,...
"""

import base64
import logging
import os

logger = logging.getLogger(__name__)

HAWK_DOWNLOAD_BASE = "https://downloads.hawkdefense.com/agent"
DEFAULT_SERVICE_NAME = "HawkSoard"
DEFAULT_INSTALL_DIR = "C:\\Program Files\\HAWK\\SOAR"
LOCAL_INSTALLER_FILENAME = "hawk-soard-setup.exe"
_SERVICE_DISPLAY_NAME = "Hawk SOAR Agent"
_SERVICE_DESCRIPTION = "Hawk Security Orchestration, Automation and Response daemon"


# ---------------------------------------------------------------------------
# Public helpers
# ---------------------------------------------------------------------------

def find_local_installer(config=None):
    """Return the path of a locally cached NSIS installer, or ``None``.

    Checks (in order):

    1. Directory containing the running binary.  In a PyInstaller-frozen exe
       ``sys.executable`` is the actual exe path; in development ``sys.argv[0]``
       is used.  When hawk-soard is installed via NSIS the installer copies
       itself there (``hawk-soard-setup.exe``) so it is always available for
       push-based remote deployment.
    2. Directory derived from the ``pid_file`` config key (the agent install
       directory as written by the NSIS installer).

    Args:
        config (dict, optional): The local agent's parsed config dict.

    Returns:
        str | None: Absolute path to the installer, or ``None`` when not found.
    """
    import sys

    candidates = []

    # Frozen PyInstaller exe: sys.executable is the real exe path.
    # Development: use sys.argv[0].
    if getattr(sys, "frozen", False):
        base = os.path.dirname(os.path.abspath(sys.executable))
    else:
        base = os.path.dirname(os.path.abspath(sys.argv[0]))
    candidates.append(os.path.join(base, LOCAL_INSTALLER_FILENAME))

    if config:
        pid_file = config.get("pid_file", "")
        if pid_file:
            install_dir = os.path.dirname(os.path.abspath(pid_file))
            candidate = os.path.join(install_dir, LOCAL_INSTALLER_FILENAME)
            if candidate not in candidates:
                candidates.append(candidate)

    for path in candidates:
        if os.path.isfile(path):
            logger.debug("find_local_installer: found %s" % path)
            return path

    return None


def build_config_content(hawk_server, access_token, secret_key, install_dir, insecure=False):
    """Build the hawk-soard.cfg INI file content for a newly deployed agent.

    Args:
        hawk_server:  HAWK platform URL (e.g. https://ir.hawk.io).
        access_token: Service-account access_token for the remote agent.
        secret_key:   Service-account secret_key for the remote agent.
        install_dir:  Install directory on the remote host.
        insecure:     If True, write ``insecure = true``.

    Returns:
        str: INI-formatted config file content.
    """
    idir = install_dir.rstrip("\\")
    return (
        "[SETTINGS]\n"
        "server = {server}\n"
        "access_token = {token}\n"
        "secret_key = {secret}\n"
        "insecure = {insecure}\n"
        "timeout = 30\n"
        "retry = 3\n"
        "pid_file = {idir}\\hawk-soard.pid\n"
        "private_key = {idir}\\hawk-soard.key\n"
        "public_key = {idir}\\hawk-soard.crt\n"
        "client_id_file = {idir}\\hawk-soard-id\n"
        "enforce_signed_hybrid_tasks = true\n"
        "enforce_group_id = true\n"
        "max_task_timeout = 300\n"
        "enable_command_execution = true\n"
        "auto_upgrade = true\n"
        "auto_upgrade_interval = 3600\n"
        "\n"
        "[MANAGER]\n"
        "poll = 1\n"
        "block = 1\n"
        "db_type = sqlite\n"
        "\n"
        "[PLUGINS]\n"
        "data_path = {idir}\\data\n"
    ).format(
        server=hawk_server,
        token=access_token,
        secret=secret_key,
        insecure="true" if insecure else "false",
        idir=idir,
    )


def build_deploy_script(
    target_host,
    user_ref,
    password,
    hawk_server,
    access_token,
    secret_key,
    install_dir=None,
    service_name=None,
    source_url=None,
    source_path=None,
    insecure=False,
    ps_quote_fn=None,
):
    """Build a PowerShell script that deploys hawk-soard to a remote Windows host.

    The returned string is a complete PowerShell command ready to pass to
    ``["powershell", "-NoProfile", "-NonInteractive", "-Command", script]``.

    Both strategies (push and download) unconditionally write the config file
    via PowerShell after the installer runs, then perform a service restart, so
    credentials are always current even when re-deploying to a host that already
    has hawk-soard installed.

    Args:
        target_host:  Remote hostname or IP address (must have WinRM enabled).
        user_ref:     WinRM credential user, e.g. ``"DOMAIN\\\\admin"``.
        password:     WinRM credential password.
        hawk_server:  HAWK platform URL written into the remote agent config.
        access_token: Service-account access_token for the remote agent.
        secret_key:   Service-account secret_key for the remote agent.
        install_dir:  Destination directory on the remote host.
                      Defaults to ``C:\\\\Program Files\\\\HAWK\\\\SOAR``.
        service_name: Windows service name.  Defaults to ``HawkSoard``.
        source_url:   URL the remote host downloads the hawk-soard package from.
                      Only used when ``source_path`` is not provided.
                      Defaults to the HAWK download server URL.
        source_path:  Local filesystem path to the NSIS installer on the
                      deploying host.  When provided the file is pushed to the
                      remote host via ``Copy-Item -ToSession`` (preferred).
        insecure:     Write ``insecure = true`` in the remote agent config.
        ps_quote_fn:  Optional callable ``(str) -> str`` for PowerShell string
                      escaping.  Uses the built-in single-quote escaper by default.

    Returns:
        str: Complete PowerShell command.
    """
    q = ps_quote_fn or _ps_quote
    idir = (install_dir or DEFAULT_INSTALL_DIR).rstrip("\\")
    svc = service_name or DEFAULT_SERVICE_NAME

    # Always build cfg_b64 for both strategies.  The NSIS installer has an
    # IfFileExists guard that skips config-writing when a prior config exists,
    # so we always write the config via PowerShell afterwards to ensure
    # credentials are current on re-deployment.
    cfg_text = build_config_content(hawk_server, access_token, secret_key, idir, insecure)
    cfg_b64 = base64.b64encode(cfg_text.encode("utf-8")).decode("ascii")

    if source_path and os.path.isfile(source_path):
        logger.info(
            "build_deploy_script: push strategy — local installer %s -> %s"
            % (source_path, target_host)
        )
        return _build_push_script(
            target_host, user_ref, password, access_token, secret_key,
            idir, svc, source_path, cfg_b64, q,
        )

    if not source_url:
        source_url = "%s/latest/%s" % (HAWK_DOWNLOAD_BASE.rstrip("/"), LOCAL_INSTALLER_FILENAME)

    logger.info(
        "build_deploy_script: download strategy — %s -> %s" % (source_url, target_host)
    )
    return _build_url_script(
        target_host, user_ref, password, access_token, secret_key,
        idir, svc, source_url, cfg_b64, q,
    )


def build_verify_script(target_host, user_ref, password, service_name=None, ps_quote_fn=None):
    """Build a lightweight PowerShell script that queries the remote service status.

    Returns:
        str: PowerShell command that emits a JSON service-status object.
    """
    q = ps_quote_fn or _ps_quote
    svc = service_name or DEFAULT_SERVICE_NAME

    remote_block = (
        "$svcName='%s';"
        "$s=Get-CimInstance Win32_Service -Filter (\"Name='\"+ $svcName +\"'\") -ErrorAction SilentlyContinue;"
        "if(-not $s){Write-Output '{\"status\":\"NotFound\",\"running\":false}';exit 0;};"
        "[PSCustomObject]@{"
        "service_name=$s.Name;status=$s.State;pid=$s.ProcessId;"
        "start_mode=$s.StartMode;running=($s.State -eq 'Running')"
        "}|ConvertTo-Json -Depth 2;"
    ) % q(svc)

    return (
        "$credSec=ConvertTo-SecureString '%s' -AsPlainText -Force;"
        "$cred=New-Object System.Management.Automation.PSCredential('%s',$credSec);"
        "Invoke-Command -ComputerName '%s' -Credential $cred -ScriptBlock {%s};"
    ) % (
        q(password),
        q(user_ref),
        q(target_host),
        remote_block,
    )


# ---------------------------------------------------------------------------
# Internal script builders
# ---------------------------------------------------------------------------

def _build_push_script(target_host, user_ref, password, access_token, secret_key,
                       idir, svc, source_path, cfg_b64, q):
    """PSSession + Copy-Item push strategy.

    1. Opens a PSSession to the remote host.
    2. Creates the install and data directories on the remote host.
    3. Copies the local NSIS installer to the remote host via Copy-Item -ToSession.
    4. Runs the installer using Start-Process so the /D= path-with-spaces
       argument is passed verbatim to NSIS (avoids PowerShell re-parsing it).
    5. Unconditionally writes the config via a base64-decoded blob so that
       credentials are current even when re-deploying over an existing install.
    6. Stops then starts the service so the new config takes effect.

    Credentials travel via -ArgumentList over the encrypted WinRM channel and
    are never embedded as string literals in the remote ScriptBlock.
    """
    # ScriptBlock 1: create directories before the file copy.
    setup_block = (
        "param($d);"
        "New-Item -ItemType Directory -Path $d -Force|Out-Null;"
        "New-Item -ItemType Directory -Path ($d+'\\data') -Force|Out-Null"
    )

    # ScriptBlock 2: run installer, write config, restart service.
    # Parameters: idir, svcName, tok (access_token), secKey (secret_key), cfgB64.
    # Note: $secKey avoids any name collision with the outer $credSec variable.
    install_block = (
        "param($idir,$svcName,$tok,$secKey,$cfgB64);"
        "$tmp=$idir+'\\hawk-soard-setup.exe';"
        "try{"
        # Use Start-Process so PowerShell does not re-tokenise the argument
        # string.  NSIS requires /D= to be the last argument and its path must
        # not be quoted — Start-Process -ArgumentList passes the string verbatim
        # to CreateProcess, which is exactly what NSIS expects.
        "$nsisArgs='/S /ACCESS_TOKEN='+$tok+' /SECRET_KEY='+$secKey+' /D='+$idir;"
        "Start-Process -FilePath $tmp -ArgumentList $nsisArgs -Wait -WindowStyle Hidden -ErrorAction Stop;"
        "Start-Sleep -Seconds 5;"
        # Always overwrite config to guarantee correct credentials on re-deploy.
        "$cfgBytes=[Convert]::FromBase64String($cfgB64);"
        "$cfgText=[Text.Encoding]::UTF8.GetString($cfgBytes);"
        "Set-Content -Path ($idir+'\\hawk-soard.cfg') -Value $cfgText -Encoding UTF8 -Force;"
        # Stop then start so the service reads the freshly written config.
        "Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue;"
        "Start-Sleep -Seconds 2;"
        "Start-Service -Name $svcName -ErrorAction Stop;"
        "Start-Sleep -Seconds 3;"
        "$s=Get-CimInstance Win32_Service -Filter (\"Name='\"+ $svcName +\"'\") -ErrorAction SilentlyContinue;"
        "$st=if($s){$s.State}else{'NotFound'};"
        "$spid=if($s){$s.ProcessId}else{0};"
        "[PSCustomObject]@{"
        "deployed=$true;service_name=$svcName;status=$st;pid=$spid;"
        "install_dir=$idir;exe_exists=(Test-Path ($idir+'\\hawk-soard.exe'));config_written=$true"
        "}|ConvertTo-Json -Depth 2;"
        "}catch{"
        "[PSCustomObject]@{deployed=$false;error=$_.Exception.Message;service_name=$svcName;install_dir=$idir}"
        "|ConvertTo-Json -Depth 2;}"
    )

    remote_installer_path = idir + "\\" + LOCAL_INSTALLER_FILENAME

    # Outer script: session lifecycle with catch so failures always emit JSON.
    # $credSec is used for the local SecureString to avoid any name clash with
    # the $secKey parameter inside the remote ScriptBlock.
    return (
        "$credSec=ConvertTo-SecureString '%s' -AsPlainText -Force;"
        "$cred=New-Object System.Management.Automation.PSCredential('%s',$credSec);"
        "$sess=New-PSSession -ComputerName '%s' -Credential $cred -ErrorAction Stop;"
        "try{"
        "Invoke-Command -Session $sess -ScriptBlock {%s} -ArgumentList '%s' -ErrorAction Stop;"
        "Copy-Item -Path '%s' -Destination '%s' -ToSession $sess -Force -ErrorAction Stop;"
        "Invoke-Command -Session $sess -ScriptBlock {%s} -ArgumentList '%s','%s','%s','%s','%s';"
        "}catch{"
        "[PSCustomObject]@{deployed=$false;error=$_.Exception.Message;service_name='%s';install_dir='%s'}"
        "|ConvertTo-Json -Depth 2;"
        "}finally{"
        "if($sess){Remove-PSSession $sess -ErrorAction SilentlyContinue}}"
    ) % (
        q(password),
        q(user_ref),
        q(target_host),
        setup_block,
        q(idir),
        q(source_path),
        q(remote_installer_path),
        install_block,
        q(idir),
        q(svc),
        q(access_token),
        q(secret_key),
        q(cfg_b64),
        q(svc),
        q(idir),
    )


def _build_url_script(target_host, user_ref, password, access_token, secret_key,
                      idir, svc, source_url, cfg_b64, q):
    """Self-contained Invoke-Command download-and-install strategy.

    Downloads the package from source_url on the remote host.

    - ``.exe`` packages: run via Start-Process with NSIS CLI args.
    - ``.zip`` packages: extract then register the service manually.

    In both cases the config is unconditionally written via a base64-decoded
    PowerShell blob and the service is restarted so credentials are always
    current, including on re-deployment.
    """
    remote_block = (
        "$idir='%s';"
        "$svcName='%s';"
        "$accessToken='%s';"
        "$secretKey='%s';"
        "$exePath=$idir+'\\hawk-soard.exe';"
        "$cfgPath=$idir+'\\hawk-soard.cfg';"
        "$srcUrl='%s';"
        "$cfgB64='%s';"
        # Derive temp-file extension from the URL, not the fixed filename, so
        # the .exe/.zip branch detection further down is always correct.
        "$tmpExt=if($srcUrl -match '\\.zip([?#]|$)'){'zip'}else{'exe'};"
        "$tmp=$env:TEMP+'\\hawk-soard-dl.'+$tmpExt;"
        "try{"
        # 1. Ensure directories exist.
        "New-Item -ItemType Directory -Path $idir -Force|Out-Null;"
        "New-Item -ItemType Directory -Path ($idir+'\\data') -Force|Out-Null;"
        # 2. Download and install only when the binary is not already present.
        "if(-not(Test-Path $exePath)){"
        "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;"
        "Invoke-WebRequest -Uri $srcUrl -OutFile $tmp -UseBasicParsing -ErrorAction Stop;"
        "if($tmpExt -eq 'exe'){"
        # Use Start-Process so the /D= path-with-spaces reaches NSIS verbatim.
        "$nsisArgs='/S /ACCESS_TOKEN='+$accessToken+' /SECRET_KEY='+$secretKey+' /D='+$idir;"
        "Start-Process -FilePath $tmp -ArgumentList $nsisArgs -Wait -WindowStyle Hidden -ErrorAction Stop;"
        "Start-Sleep -Seconds 5;"
        "}"
        "elseif($tmpExt -eq 'zip'){"
        "Expand-Archive -Path $tmp -DestinationPath $idir -Force;"
        # Manual service registration for zip installs.
        "$binPath='\"'+$exePath+'\" -c \"'+$cfgPath+'\" -service';"
        "$existing=Get-Service -Name $svcName -ErrorAction SilentlyContinue;"
        "if($existing){"
        "Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue;"
        "sc.exe config $svcName binPath= $binPath start= auto|Out-Null;}"
        "else{"
        "New-Service -Name $svcName -BinaryPathName $binPath "
        "-DisplayName '%s' -Description '%s' -StartupType Automatic|Out-Null;}"
        "};"
        "Remove-Item $tmp -Force -ErrorAction SilentlyContinue;"
        "};"
        # 3. Always write config — guarantees credentials are current even when
        #    the binary was already installed and the NSIS guard skipped writing.
        "$cfgBytes=[Convert]::FromBase64String($cfgB64);"
        "$cfgText=[Text.Encoding]::UTF8.GetString($cfgBytes);"
        "Set-Content -Path $cfgPath -Value $cfgText -Encoding UTF8 -Force;"
        # 4. Restart service so the new config takes effect.
        "Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue;"
        "Start-Sleep -Seconds 2;"
        "Start-Service -Name $svcName -ErrorAction Stop;"
        "Start-Sleep -Seconds 3;"
        # 5. Report status.
        "$s=Get-CimInstance Win32_Service -Filter (\"Name='\"+ $svcName +\"'\") -ErrorAction SilentlyContinue;"
        "$st=if($s){$s.State}else{'NotFound'};"
        "$spid=if($s){$s.ProcessId}else{0};"
        "[PSCustomObject]@{"
        "deployed=$true;service_name=$svcName;status=$st;pid=$spid;"
        "install_dir=$idir;exe_exists=(Test-Path $exePath);config_written=$true"
        "}|ConvertTo-Json -Depth 2;"
        "}catch{"
        "[PSCustomObject]@{"
        "deployed=$false;error=$_.Exception.Message;service_name=$svcName;install_dir=$idir"
        "}|ConvertTo-Json -Depth 2;"
        "}"
    ) % (
        q(idir),
        q(svc),
        q(access_token),
        q(secret_key),
        q(source_url),
        q(cfg_b64),
        q(_SERVICE_DISPLAY_NAME),
        q(_SERVICE_DESCRIPTION),
    )

    return (
        "$credSec=ConvertTo-SecureString '%s' -AsPlainText -Force;"
        "$cred=New-Object System.Management.Automation.PSCredential('%s',$credSec);"
        "Invoke-Command -ComputerName '%s' -Credential $cred -ScriptBlock {%s};"
    ) % (
        q(password),
        q(user_ref),
        q(target_host),
        remote_block,
    )


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _ps_quote(value):
    """Escape a value for safe embedding inside a PowerShell single-quoted string."""
    if value is None:
        return ""
    return str(value).replace("'", "''")
