<#
.SYNOPSIS
    Updates the local installed restic backup scripts from GitHub,
    either using the latest tagged release or by targeting a specific branch.

.DESCRIPTION
    This script supports two modes:

    1. **Release mode (default):**
       - Fetches the latest release info via GitHub’s API.
       - Compares the release tag (after normalization) against a locally stored version (in state.xml).
       - If the GitHub release is newer, downloads the release zip, extracts it, copies the files
         over the local installation.

    2. **Branch mode:**
       - Targets a specific branch (default "main") by retrieving branch information from GitHub.
       - Compares the latest commit SHA on that branch against a locally stored SHA (in state.xml).
       - If the remote commit SHA differs, downloads the branch zip archive, extracts it,
         copies the files over the local installation.

.NOTES
    Example 1 - update scripts to the latest tagged release
    .\update.ps1

    Example 2 - update scripts from a branch
    .\update.ps1 -Mode branch -BranchName 'release_1.8'

    Example 3 - download a new copy of the update scripts and run it
    1. Change your directory to your installation directory (e.g. `cd c:\restic`)
    2. Invoke-WebRequest "https://raw.githubusercontent.com/kmwoley/restic-windows-backup/main/update.ps1" -OutFile update.ps1
    3. .\update.ps1
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [ValidateSet("release", "branch")]
    [string]$Mode = "release",

    [Parameter(Mandatory = $false)]
    [string]$BranchName = "main",

    [Parameter(Mandatory = $false)]
    [string]$InstallPath = $null
)

# ====================================
# Configuration and Setup
# ====================================

# GitHub repository details
$repoOwner = "kmwoley"
$repoName  = "restic-windows-backup"

# User-Agent header (GitHub requires this)
$headers = @{ "User-Agent" = "PowerShell" }

# default the installation directory to the location of the running script
if([string]::IsNullOrEmpty($InstallPath)) {
    # default to the script's location, if running as a script
    $InstallPath = $PSScriptRoot
    if([string]::IsNullOrEmpty($InstallPath)) {
        # default to the current working directory, if not running as a script
        $InstallPath = Get-Location
    }
}

# ====================================
# Functions for state management
# ====================================
function Get-State {
    if(Test-Path $Script:StateFile) {
        Import-Clixml $Script:StateFile | ForEach-Object{ Set-Variable -Scope Script $_.Name $_.Value }
    }
}
function Set-State {
    Get-Variable ResticState* | Export-Clixml $Script:StateFile
}

# ===========================================
# Functions for file management and download
# ===========================================
function Get-ModifiedFiles {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Source,

        [Parameter(Mandatory = $true)]
        [string]$Destination,

        [Parameter(Mandatory = $true)]
        [string]$DateTime
    )

    $modifiedFiles = New-Object System.Collections.Generic.List[System.Object]

    if(-not (Test-Path $Source)) {
        Write-Error "Source does not exist ($Source)"
        exit 1
    }
    if(-not (Test-Path $Destination)) {
        Write-Error "Destination does not exist ($Destination)"
        exit 1
    }

    $sourceFiles = Get-ChildItem $Source

    ForEach ($sourceFile in $sourceFiles) {
        # find if there's a corrosponding file in the destination
        $destFileName = Join-Path $Destination $sourceFile.Name
        if(Test-Path $destFileName) {
            $destFile = Get-ChildItem $destFileName
            if($destFile.LastWriteTime -gt $DateTime) {
                # destination file has been modified after $DateTime
                $modifiedFiles.Add($destFile.FullName)
            }
        }
    }
    return $modifiedFiles
}

function Update-InstalledScripts {
    param(
        [Parameter(Mandatory=$true)][string]$ZipUrl,
        [Parameter(Mandatory=$true)][string]$DestinationFolder
    )

    $timestamp = Get-Date -Format FileDateTime
    $tempExtractDir = Join-Path $env:TEMP ("restic-windows-backup." + $timestamp)
    $tempZipPath = Join-Path $env:TEMP ("restic-windows-backup." + $timestamp + ".zip")

    # test temp location, fail if in use
    if (Test-Path $tempExtractDir) {
        Write-Error "Temporary directory already exists: $tempExtractDir"
        exit 1
    }
    if (Test-Path $tempZipPath) {
        Write-Error "Temporary directory already exists: $tempZipPath"
        exit 1
    }

    # Create a temporary folder for extraction
    New-Item -ItemType Directory -Path $tempExtractDir | Out-Null

    Write-Host "Downloading from: $ZipUrl"
    try {
        Invoke-WebRequest -Uri $ZipUrl -OutFile $tempZipPath -Headers $headers
    } catch {
        Write-Error "Failed to download the file: $_"
        exit 1
    }

    try {
        Expand-Archive -LiteralPath $tempZipPath $tempExtractDir
    } catch {
        Write-Error "Error extracting zip file: $_"
        exit 1 
    }

    # Determine the actual folder containing the repository files.
    $extractedContent = Get-ChildItem -Path $tempExtractDir | Where-Object { $_.PSIsContainer }
    if ($extractedContent.Count -eq 1) {
        $extractedFolder = $extractedContent[0].FullName
    } else {
        $extractedFolder = $tempExtractDir
    }

    # Check to make sure not to overwrite modified files
    $installedDate = $Script:ResticStateInstalledDate
    if([string]::IsNullOrEmpty($installedDate)) {
        # unkown install date; setting the date
        $installedDate = [datetime]::MinValue
    }
    
    $modifiedFiles = Get-ModifiedFiles -Source $extractedFolder -Destination $DestinationFolder -DateTime $installedDate
    if($modifiedFiles) {
        if([string]::IsNullOrEmpty($Script:ResticStateInstalledDate)) {
            Write-Host "WARNING: The following files already exist in the target directory"
        }        
        else {
            Write-Host "WARNING: The following files have been modified since they were installed on $installedDate"
        }
        ForEach ($fileName in $modifiedFiles) {
            Write-Host " - " $fileName
        }
        
        # TODO: add a "-Force" parameter to skip this check/question
        Write-Host "Continuing will overwrite these files."
        Write-host "Do you want to continue?"
        $userInput = Read-Host "[Y] Yes  [N] No  (default is ""Y"")"
        if ($userInput -ieq 'n') {
            Write-Host "Operation cancelled."
            exit 0
        }
    }

    Write-Host "Updating files in installation directory ($DestinationFolder)..."
    try {
        # Recursively copy all content from the extracted folder to the local directory.
        Copy-Item -Path (Join-Path $extractedFolder "*") -Destination $DestinationFolder -Recurse -Force
    } catch {
        Write-Error "Error copying files: $_"
        exit 1
    }

    # Clean up temporary files
    Remove-Item $tempZipPath -Force
    Remove-Item $tempExtractDir -Recurse -Force
}

# ====================================
# Main
# ====================================

# load restic state
$Script:ResticStateInstalledVersion = $null
$Script:ResticStateInstalledBranchSHA = $null
$Script:ResticStateInstalledDate = $null
$Script:StateFile = Join-Path $InstallPath "state.xml"
Get-State

# ====================================
# Release mode
# ====================================

if ($Mode -eq "release") {

    # Read the version of the scripts installed
    $localVersion = $Script:ResticStateInstalledVersion
    if ([string]::IsNullOrEmpty($localVersion)) {
        # No version information stored locally
        $localVersion = "0.0.0"
    }
  
    # Get the Latest Release Info from GitHub
    $releaseApiUrl = "https://api.github.com/repos/$repoOwner/$repoName/releases/latest"
    try {
        Write-Host "Checking GitHub for latest release of '$repoOwner/$repoName'..."
        $release = Invoke-RestMethod -Uri $releaseApiUrl -Headers $headers
    } catch {
        Write-Error "Error fetching release information from GitHub: $_"
        exit 1
    }

    $latestTagRaw = $release.tag_name
    $latestTag    = $latestTagRaw.Trim()

    # Normalize versions (remove leading "v" if present)
    function Get-NormalizedVersion($versionString) {
        if ($versionString.StartsWith("v", [System.StringComparison]::InvariantCultureIgnoreCase)) {
            return $versionString.Substring(1)
        }
        return $versionString
    }
    $normalizedLocalVersion  = Get-NormalizedVersion $localVersion
    $normalizedLatestVersion = Get-NormalizedVersion $latestTag

    try {
        $localVersionObj  = [Version]$normalizedLocalVersion
        $latestVersionObj = [Version]$normalizedLatestVersion
    } catch {
        Write-Error "Error parsing version strings. Local: $normalizedLocalVersion, Latest: $normalizedLatestVersion. $_"
        exit 1
    }

    if ($latestVersionObj -le $localVersionObj) {
        Write-Host "Installed version ($localVersionObj) is up-to-date. No update needed."
        exit 0
    } else {
        Write-Host "Newer release available: $latestVersionObj (installed: $localVersionObj). Proceeding with update..."
    }

    # get the zip URL from the release info
    $zipUrl = $release.zipball_url

    # Download and update the installed scripts
    Update-InstalledScripts -ZipUrl $zipUrl -DestinationFolder $InstallPath

    # Store the installed version number and time installed
    $Script:ResticStateInstalledVersion = $normalizedLatestVersion
    $Script:ResticStateInstalledDate = Get-Date
    $Script:ResticStateInstalledBranchSHA = $null
    Set-State

    Write-Host "Update successful. Installed version is now $normalizedLatestVersion."
}
# ====================================
# Branch mode
# ====================================
elseif ($Mode -eq "branch") {

    # Read the SHA of the branch source installed
    $localCommitSHA = $Script:ResticStateInstalledBranchSHA
    if ([string]::IsNullOrEmpty($localCommitSHA)) {
        # Write-Host "No branch information stored locally."
        $localCommitSHA = "unknown"
    }

    # Retrieve branch information from GitHub
    $branchApiUrl = "https://api.github.com/repos/$repoOwner/$repoName/branches/$BranchName"
    try {
        Write-Host "Checking GitHub for latest commit of '$repoOwner/$repoName' on branch '$BranchName'..."
        $branchInfo = Invoke-RestMethod -Uri $branchApiUrl -Headers $headers
    } catch {
        Write-Error "Error fetching branch information from GitHub: $_"
        exit 1
    }

    $latestCommitSHA = $branchInfo.commit.sha

    if ($localCommitSHA -eq $latestCommitSHA) {
        Write-Host "Installed commit ($latestCommitSHA) is up-to-date. No update needed."
        exit 0
    } else {
        Write-Host "Latest commit: $latestCommitSHA (installed: $localCommitSHA). Proceeding with update..."
    }

    # Construct the zip URL for the branch.
    # GitHub provides branch archives at:
    # https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip
    $zipUrl = "https://github.com/$repoOwner/$repoName/archive/refs/heads/$BranchName.zip"

    # Download and update the installed scripts
    Update-InstalledScripts -ZipUrl $zipUrl -DestinationFolder $InstallPath

    # Store the installed branch commit SHA and time installed
    $Script:ResticStateInstalledVersion = $null
    $Script:ResticStateInstalledDate = Get-Date
    $Script:ResticStateInstalledBranchSHA = $latestCommitSHA
    Set-State

    Write-Host "Update successful. Local branch is now at commit $latestCommitSHA."
}
else {
    Write-Error "Unsupported mode."
    exit 1
}
