# ArcIn Connector Agent - Windows installer
#
# Installs the agent as a Windows Service.
#
# Standard usage (from inside the extracted bundle, either at its root
# or from within the scripts\ directory):
#   powershell -ExecutionPolicy Bypass -File .\install-agent-windows.ps1
#
# The installer auto-locates the Windows binary at ..\bin\ relative to
# itself and the example config at ..\configs\ relative to itself.
#
# Override either with explicit paths if needed:
#   -BinarySrc    path to arcin-agent.exe
#   -ExampleCfg   path to agent.example.yaml

[CmdletBinding()]
param(
    [string]$BinarySrc  = "",
    [string]$ExampleCfg = ""
)

$ErrorActionPreference = 'Stop'

# --- Resolve bundle-relative defaults from the script's own location ---
$BundleRoot = Split-Path -Parent $PSScriptRoot
if (-not $BinarySrc)  { $BinarySrc  = Join-Path $BundleRoot 'bin\arcin-agent-windows-amd64.exe' }
if (-not $ExampleCfg) { $ExampleCfg = Join-Path $BundleRoot 'configs\agent.example.yaml' }

# --- Constants ---
$ServiceName  = 'ArcInAgent'
$DisplayName  = 'ArcIn Connector Agent'
$Description  = 'Outbound-only connector bridging ArcIn SaaS to local databases.'
$InstallDir   = "$env:ProgramFiles\ArcInAgent"
$BinaryDst    = Join-Path $InstallDir 'arcin-agent.exe'
$HostExe      = Join-Path $InstallDir 'arcin-service-host.exe'
$ConfigDir    = "$env:ProgramData\ArcInAgent"
$ConfigFile   = Join-Path $ConfigDir 'agent.yaml'
$LogDir       = Join-Path $ConfigDir 'logs'
$LogFile      = Join-Path $LogDir    'agent.log'

function Assert-Administrator {
    $current = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($current)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-Error "This script must be run as Administrator."
        exit 1
    }
}

function Assert-Binary {
    if (-not (Test-Path $BinarySrc)) {
        Write-Error "Binary not found at $BinarySrc."
        exit 1
    }
    Write-Host "Using binary: $BinarySrc"
    Write-Host "Using example config: $ExampleCfg"
}

function Install-Binary {
    Write-Host "Creating $InstallDir"
    New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
    Write-Host "Copying binary to $BinaryDst"
    Copy-Item -Path $BinarySrc -Destination $BinaryDst -Force
}

function Install-Config {
    New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null
    New-Item -ItemType Directory -Force -Path $LogDir    | Out-Null

    if (-not (Test-Path $ConfigFile)) {
        Write-Host "Installing config to $ConfigFile (edit before starting)"
        # Write a clean minimal template - avoids YAML parse errors from
        # users accidentally editing inside the verbose comment blocks.
        @"
agent:
  customer_id: "REPLACE_ME"
  agent_id: ""
  sources:
    - id: "mysql-prod"
      type: "mysql"
      dsn_env_var: "ARCIN_SOURCE_MYSQL_PROD_DSN"
      role: "readonly"

control_plane:
  url: "wss://REPLACE_ME:8443/connect"
  token: "REPLACE_ME"
  heartbeat_interval: 30s
  handshake_timeout: 10s
  reconnect_min_wait: 1s
  reconnect_max_wait: 60s

log:
  level: info
  format: json
"@ | Set-Content -Path $ConfigFile -Encoding UTF8
    } else {
        Write-Host "$ConfigFile already exists, not overwriting."
    }

    # Restrict config file ACL: SYSTEM + Administrators only.
    Write-Host "Hardening ACL on $ConfigFile"
    $acl = Get-Acl $ConfigFile
    $acl.SetAccessRuleProtection($true, $false)
    foreach ($existing in @($acl.Access)) { [void]$acl.RemoveAccessRule($existing) }

    $ruleSystem = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule `
        -ArgumentList 'NT AUTHORITY\SYSTEM', 'FullControl', 'Allow'
    $ruleAdmins = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule `
        -ArgumentList 'BUILTIN\Administrators', 'FullControl', 'Allow'
    $acl.AddAccessRule($ruleSystem)
    $acl.AddAccessRule($ruleAdmins)
    Set-Acl -Path $ConfigFile -AclObject $acl
}

function Build-ServiceHost {
    # The arcin-agent binary signals SERVICE_RUNNING to the Windows SCM only after
    # a successful control-plane handshake. This causes error 1053 (30s timeout)
    # whenever the handshake takes longer than SCM allows - even though the binary
    # itself connects fine. The fix: compile a thin .NET ServiceBase host that
    # signals SERVICE_RUNNING immediately, then runs arcin-agent as a child process.
    Write-Host "Building service host wrapper..."

    $src = @"
using System;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;

public sealed class ArcInServiceHost : ServiceBase
{
    private Process _agent;
    private const string BinaryPath = @"$BinaryDst";
    private const string ConfigPath = @"$ConfigFile";
    private const string LogFile    = @"$LogFile";

    public ArcInServiceHost()
    {
        ServiceName = "$ServiceName";
        CanStop     = true;
        CanShutdown = true;
        AutoLog     = false;
    }

    protected override void OnStart(string[] args)
    {
        var psi = new ProcessStartInfo
        {
            FileName               = BinaryPath,
            Arguments              = "--config \"" + ConfigPath + "\"",
            UseShellExecute        = false,
            CreateNoWindow         = true,
            RedirectStandardOutput = true,
            RedirectStandardError  = true
        };
        _agent = new Process { StartInfo = psi, EnableRaisingEvents = true };
        _agent.OutputDataReceived += (s, e) => AppendLog(e.Data);
        _agent.ErrorDataReceived  += (s, e) => AppendLog(e.Data);
        _agent.Exited += (s, e) => AppendLog("arcin-agent exited code=" + _agent.ExitCode);
        _agent.Start();
        _agent.BeginOutputReadLine();
        _agent.BeginErrorReadLine();
        // Return immediately - SCM receives SERVICE_RUNNING signal right away.
        // arcin-agent handles its own reconnect loop independently.
    }

    protected override void OnStop()     { KillAgent(); }
    protected override void OnShutdown() { KillAgent(); }

    private void KillAgent()
    {
        try {
            if (_agent != null && !_agent.HasExited) {
                _agent.Kill();
                _agent.WaitForExit(5000);
            }
        } catch {}
    }

    private static void AppendLog(string msg)
    {
        if (string.IsNullOrEmpty(msg)) return;
        try {
            Directory.CreateDirectory(Path.GetDirectoryName(LogFile));
            File.AppendAllText(LogFile, DateTime.UtcNow.ToString("o") + " " + msg + "\n");
        } catch {}
    }

    public static void Main() { ServiceBase.Run(new ArcInServiceHost()); }
}
"@

    # Find csc.exe (.NET Framework C# compiler, ships with Windows)
    $csc = Get-ChildItem "$env:windir\Microsoft.NET\Framework64" -Filter 'csc.exe' -Recurse -ErrorAction SilentlyContinue |
           Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
    if (-not $csc) {
        $csc = Get-ChildItem "$env:windir\Microsoft.NET\Framework" -Filter 'csc.exe' -Recurse -ErrorAction SilentlyContinue |
               Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
    }
    if (-not $csc) { Write-Error "csc.exe not found. .NET Framework is required."; exit 1 }
    Write-Host "  Using compiler: $csc"

    $srcFile = Join-Path $env:TEMP 'arcin-service-host.cs'
    $src | Set-Content -Path $srcFile -Encoding UTF8

    $svcDll = [Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() + 'System.ServiceProcess.dll'
    $result  = & $csc /nologo /target:winexe /out:"$HostExe" /reference:"$svcDll" "$srcFile" 2>&1
    if ($LASTEXITCODE -ne 0) {
        Write-Error "Compilation failed:`n$result"
        exit 1
    }
    Remove-Item $srcFile -Force -ErrorAction SilentlyContinue
    Write-Host "  Built: $HostExe"
}

function Remove-ExistingService {
    $existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "Existing service found; stopping and removing"
        if ($existing.Status -ne 'Stopped') { Stop-Service -Name $ServiceName -Force }
        & sc.exe delete $ServiceName | Out-Null
        Start-Sleep -Seconds 2
    }
}

function Register-Service {
    Write-Host "Registering Windows service: $ServiceName"

    # Register the service host wrapper, not arcin-agent directly.
    # The host signals SERVICE_RUNNING immediately then manages arcin-agent as a child.
    New-Service -Name $ServiceName `
        -DisplayName $DisplayName `
        -Description $Description `
        -BinaryPathName "`"$HostExe`"" `
        -StartupType Automatic | Out-Null

    # Restart on failure: 5s delay, reset counter after 1 day.
    & sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/5000/restart/5000 | Out-Null
}

function Show-NextSteps {
    Write-Host ""
    Write-Host "Install complete." -ForegroundColor Green
    Write-Host "Next steps:"
    Write-Host "  1. Edit $ConfigFile - set customer_id, token, control_plane.url"
    Write-Host "  2. Start-Service $ServiceName"
    Write-Host "  3. Get-Service $ServiceName"
    Write-Host "  4. Agent logs: $LogFile"
}

# --- Main ---
Assert-Administrator
Assert-Binary
Install-Binary
Install-Config
Build-ServiceHost
Remove-ExistingService
Register-Service
Show-NextSteps
