<#
.SYNOPSIS
    Migrates BirthBiologic Datto Workplace files to SharePoint Online.

.DESCRIPTION
    Reads files from C:\Users\Public\Desktop\Datto Workplace Server Projects on BB-SERVER
    and uploads them to the corresponding SharePoint document libraries via Microsoft Graph API.
    Supports resume: already-uploaded files are skipped based on a state file.

.PARAMETER ClientSecret
    App secret for the ComputerGuru Tenant Admin app (tenant-admin tier).
    Can also be set via env var BB_MIGRATION_SECRET.

.PARAMETER Token
    Pre-obtained Graph Bearer token. Use if you already have a valid token.

.PARAMETER DattoRoot
    Path to the Datto Workplace root folder.
    Default: C:\Users\Public\Desktop\Datto Workplace Server Projects

.PARAMETER OnlyFolder
    Migrate only this top-level Datto folder (e.g. "Donor Services").
    Omit to migrate all folders.

.PARAMETER WhatIf
    Dry run. Lists what would be uploaded without transferring anything.

.PARAMETER Resume
    Skip files already recorded in the state file. Default: true.
    Pass -Resume:$false to force re-upload everything.

.EXAMPLE
    # Dry run on one folder
    .\migrate-datto-to-sharepoint.ps1 -ClientSecret "xxx" -OnlyFolder "Supply Management" -WhatIf

.EXAMPLE
    # Real migration, one folder at a time
    .\migrate-datto-to-sharepoint.ps1 -ClientSecret "xxx" -OnlyFolder "Supply Management"

.NOTES
    Requires ComputerGuru Tenant Admin app consented in BirthBiologic tenant
    with Sites.ReadWrite.All application permission.
    Run on BB-SERVER where Datto files are locally accessible.
#>

[CmdletBinding()]
param(
    [string]$ClientSecret = $env:BB_MIGRATION_SECRET,
    [string]$Token        = "",
    [string]$DattoRoot    = "C:\Users\Public\Desktop\Datto Workplace Server Projects",
    [string]$OnlyFolder   = "",
    [switch]$WhatIf,
    [bool]$Resume         = $true,
    [switch]$DeltaOnly
)

$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Constants
$TENANT_ID  = "19a568e8-9e88-413b-9341-cbc224b39145"
$CLIENT_ID  = "709e6eed-0711-4875-9c44-2d3518c47063"
$GRAPH_ROOT = "https://graph.microsoft.com/v1.0"
$CHUNK_SIZE = 60 * 1024 * 1024
$SMALL_FILE = 4  * 1024 * 1024
$MAX_RETRY  = 3

$LOG_DIR    = "C:\GuruMigration"
$LOG_FILE   = "$LOG_DIR\bb-migration-$(Get-Date -Format 'yyyy-MM-dd').log"
$STATE_FILE = "$LOG_DIR\bb-migration-state.json"

# Site ID map: Datto folder name -> SharePoint site ID
$SITE_MAP = [ordered]@{
    "Admin"                           = "birthbiologic.sharepoint.com,1baf65c1-c4b3-4602-9111-1f99ae800023,4a1a8706-42a5-4d72-bb7d-75a846036623"
    "Birth Biologic Activity Reports" = "birthbiologic.sharepoint.com,1baf65c1-c4b3-4602-9111-1f99ae800023,4a1a8706-42a5-4d72-bb7d-75a846036623"
    "Donor Services"                  = "birthbiologic.sharepoint.com,bcbfa272-dc85-424c-af66-3f14c75ffeb4,8b0975dd-e0ac-4ad0-9921-d7c9c2858865"
    "ITSvcs"                          = "birthbiologic.sharepoint.com,83b636d9-2e38-4fd8-83eb-431ab1f0067b,8b0975dd-e0ac-4ad0-9921-d7c9c2858865"
    "Quality Department"              = "birthbiologic.sharepoint.com,5fd38089-b87f-4472-8d17-2d8bbc2759a2,8b0975dd-e0ac-4ad0-9921-d7c9c2858865"
    "Supply Management"               = "birthbiologic.sharepoint.com,4700ecf3-25ba-41b6-918c-9fe620038172,8b0975dd-e0ac-4ad0-9921-d7c9c2858865"
}

# Folders routed into a sub-path at the destination
$SUBFOLDER_PREFIX = @{
    "Birth Biologic Activity Reports" = "Birth Biologic Activity Reports"
}

# Logging
if (-not (Test-Path $LOG_DIR)) { New-Item -ItemType Directory -Path $LOG_DIR | Out-Null }

function Write-Log {
    param([string]$Msg, [string]$Level = "INFO")
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Msg"
    Write-Host $line
    Add-Content -Path $LOG_FILE -Value $line -Encoding UTF8
}

# State tracking for resume
$script:state = @{}
if ($Resume -and (Test-Path $STATE_FILE)) {
    $raw = Get-Content $STATE_FILE -Raw | ConvertFrom-Json
    $raw.PSObject.Properties | ForEach-Object { $script:state[$_.Name] = $_.Value }
    Write-Log "Resume state loaded: $($script:state.Count) files already uploaded"
}

function Save-State {
    $script:state | ConvertTo-Json -Depth 2 | Set-Content $STATE_FILE -Encoding UTF8
}

function Mark-Done {
    param([string]$Key)
    $script:state[$Key] = (Get-Date -Format "o")
    if ($script:state.Count % 25 -eq 0) { Save-State }
}

function Is-Done {
    param([string]$Key)
    return $script:state.ContainsKey($Key)
}

# Token management
$script:tokenValue   = ""
$script:tokenFetched = 0
$script:tokenExpiry  = 0

function Get-Token {
    $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    if ($script:tokenValue -and ($now + 300) -lt ($script:tokenFetched + $script:tokenExpiry)) {
        return $script:tokenValue
    }
    if ($Token) {
        $script:tokenValue   = $Token
        $script:tokenFetched = $now
        $script:tokenExpiry  = 3600
        return $Token
    }
    if (-not $ClientSecret) {
        throw "Provide -ClientSecret or -Token, or set env var BB_MIGRATION_SECRET."
    }
    $body = "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$([uri]::EscapeDataString($ClientSecret))&scope=https://graph.microsoft.com/.default"
    $resp = Invoke-RestMethod -Method POST `
        -Uri "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" `
        -Body $body -ContentType "application/x-www-form-urlencoded"
    $script:tokenValue   = $resp.access_token
    $script:tokenFetched = $now
    $script:tokenExpiry  = $resp.expires_in
    Write-Log "Token acquired (expires in $($resp.expires_in)s)"
    return $script:tokenValue
}

# Graph API call with retry
function Invoke-Graph {
    param(
        [string]$Method,
        [string]$Uri,
        [hashtable]$ExtraHeaders = @{},
        [object]$Body = $null,
        [string]$ContentType = "application/json",
        [int]$Attempt = 0
    )
    $tok = Get-Token
    $headers = @{ Authorization = "Bearer $tok" }
    foreach ($k in $ExtraHeaders.Keys) { $headers[$k] = $ExtraHeaders[$k] }

    $params = @{
        Method      = $Method
        Uri         = $Uri
        Headers     = $headers
        ContentType = $ContentType
    }
    if ($null -ne $Body) { $params.Body = $Body }

    try {
        return Invoke-RestMethod @params
    } catch {
        $status = 0
        if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
        if (($status -eq 429 -or $status -ge 500) -and $Attempt -lt $MAX_RETRY) {
            $wait = [math]::Pow(2, $Attempt + 1)
            Write-Log "HTTP $status - retry $($Attempt+1)/$MAX_RETRY in ${wait}s" "WARN"
            Start-Sleep -Seconds $wait
            return Invoke-Graph -Method $Method -Uri $Uri -ExtraHeaders $ExtraHeaders `
                -Body $Body -ContentType $ContentType -Attempt ($Attempt + 1)
        }
        throw
    }
}

# Encode each path segment individually
function Encode-Path {
    param([string]$RelPath)
    return ($RelPath.Split('/') | ForEach-Object { [uri]::EscapeDataString($_) }) -join '/'
}

# Upload small file (<= 4 MB)
function Upload-SmallFile {
    param([string]$SiteId, [string]$RemotePath, [string]$LocalPath)
    $encoded = Encode-Path $RemotePath
    $uri     = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}:/content"
    $bytes   = [System.IO.File]::ReadAllBytes($LocalPath)
    return Invoke-Graph -Method PUT -Uri $uri -Body $bytes -ContentType "application/octet-stream"
}

# Upload large file via chunked upload session
function Upload-LargeFile {
    param([string]$SiteId, [string]$RemotePath, [string]$LocalPath)

    $fileSize = (Get-Item $LocalPath).Length
    $encoded  = Encode-Path $RemotePath

    # Delete any existing/partial item first so upload session creation doesn't 409
    $deleteUri = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}"
    try { Invoke-Graph -Method DELETE -Uri $deleteUri | Out-Null } catch {}

    $sessionUri  = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}:/createUploadSession"
    $sessionBody = '{"item":{"@microsoft.graph.conflictBehavior":"replace"}}'
    $session     = Invoke-Graph -Method POST -Uri $sessionUri -Body $sessionBody
    $uploadUrl   = $session.uploadUrl

    $stream = [System.IO.File]::OpenRead($LocalPath)
    $buf    = New-Object byte[] $CHUNK_SIZE
    $offset = 0L
    $resp   = $null

    try {
        while ($offset -lt $fileSize) {
            $read = $stream.Read($buf, 0, $CHUNK_SIZE)
            if ($read -eq 0) { break }
            $chunk = if ($read -eq $CHUNK_SIZE) { $buf } else { $buf[0..($read - 1)] }
            $end   = $offset + $read - 1
            $pct   = [int]($offset * 100 / $fileSize)
            Write-Host "`r    $pct% ($([math]::Round($offset/1MB,0)) MB / $([math]::Round($fileSize/1MB,0)) MB)   " -NoNewline

            $attempt = 0
            $ok      = $false
            while (-not $ok) {
                try {
                    $resp = Invoke-RestMethod -Method PUT -Uri $uploadUrl `
                        -Headers @{ "Content-Range" = "bytes $offset-$end/$fileSize" } `
                        -Body $chunk -ContentType "application/octet-stream"
                    $ok = $true
                } catch {
                    $attempt++
                    if ($attempt -ge $MAX_RETRY) { throw }
                    Start-Sleep -Seconds ([math]::Pow(2, $attempt))
                }
            }
            $offset += $read
        }
    } catch {
        # Cancel the upload session so outer retries can start a fresh one
        try { Invoke-RestMethod -Method DELETE -Uri $uploadUrl -ErrorAction SilentlyContinue } catch {}
        throw
    } finally {
        $stream.Dispose()
        Write-Host ""
    }
    return $resp
}

function Upload-File {
    param([string]$SiteId, [string]$RemotePath, [string]$LocalPath)
    $size = (Get-Item $LocalPath).Length
    if ($size -le $SMALL_FILE) {
        return Upload-SmallFile -SiteId $SiteId -RemotePath $RemotePath -LocalPath $LocalPath
    } else {
        return Upload-LargeFile -SiteId $SiteId -RemotePath $RemotePath -LocalPath $LocalPath
    }
}

# Migrate one Datto top-level folder
function Migrate-Folder {
    param([string]$DattoFolder, [string]$SiteId, [string]$SitePrefix)

    $srcRoot = Join-Path $DattoRoot $DattoFolder
    if (-not (Test-Path $srcRoot)) {
        Write-Log "Source not found, skipping: $srcRoot" "WARN"
        return $null
    }

    $files  = Get-ChildItem -Path $srcRoot -Recurse -File
    $total  = $files.Count
    $done   = 0
    $skip   = 0
    $fail   = 0

    Write-Log "[$DattoFolder] $total files to process..."

    foreach ($file in $files) {
        $relPath    = $file.FullName.Substring($srcRoot.Length).TrimStart('\').Replace('\', '/')
        $remotePath = if ($SitePrefix) { "$SitePrefix/$relPath" } else { $relPath }
        $stateKey   = "$DattoFolder/$relPath"

        if ($Resume -and (Is-Done $stateKey)) {
            $skip++
            continue
        }

        $sizeMB = [math]::Round($file.Length / 1MB, 2)
        Write-Log "  [$($done + $skip + $fail + 1)/$total] $relPath ($sizeMB MB)"

        if ($DeltaOnly) {
            # Check if SharePoint already has this file and it's current
            $encoded    = Encode-Path $remotePath
            $checkUri   = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}?select=lastModifiedDateTime,size"
            try {
                $spItem    = Invoke-Graph -Method GET -Uri $checkUri
                $spDate    = [datetime]$spItem.lastModifiedDateTime
                $localDate = $file.LastWriteTimeUtc
                if ($spItem.size -gt 0 -and $spDate -ge $localDate) {
                    $skip++
                    continue
                }
                Write-Log "  [DELTA] $relPath (SP: $($spDate.ToString('yyyy-MM-dd')) / Local: $($localDate.ToString('yyyy-MM-dd')))"
            } catch {
                # Not found in SP — upload it
            }
        }

        if ($WhatIf) {
            Write-Log "  [WHATIF] -> $remotePath"
            $done++
            continue
        }

        $attempt = 0
        $ok      = $false
        while ($attempt -lt $MAX_RETRY -and -not $ok) {
            try {
                Upload-File -SiteId $SiteId -RemotePath $remotePath -LocalPath $file.FullName | Out-Null
                Mark-Done $stateKey
                $done++
                $ok = $true
            } catch {
                $attempt++
                $errMsg = $_.Exception.Message
                if ($attempt -ge $MAX_RETRY) {
                    Write-Log "  [FAIL] $relPath - $errMsg" "ERROR"
                    $fail++
                } else {
                    Write-Log "  [RETRY $attempt] $relPath - $errMsg" "WARN"
                    Start-Sleep -Seconds ([math]::Pow(2, $attempt))
                }
            }
        }
    }

    Save-State
    Write-Log "[$DattoFolder] Done: $done uploaded, $skip skipped, $fail failed / $total total"
    return [pscustomobject]@{ Folder = $DattoFolder; Uploaded = $done; Skipped = $skip; Failed = $fail; Total = $total }
}

# Main
Write-Log "=== BirthBiologic Datto -> SharePoint Migration ==="
Write-Log "Source: $DattoRoot"
Write-Log "WhatIf=$WhatIf | Resume=$Resume | DeltaOnly=$DeltaOnly | OnlyFolder=$(if ($OnlyFolder) { $OnlyFolder } else { '(all)' })"

if (-not (Test-Path $DattoRoot)) {
    Write-Log "ERROR: Datto root not found: $DattoRoot" "ERROR"
    exit 1
}

try { $null = Get-Token; Write-Log "Auth OK" }
catch { Write-Log "Auth FAILED: $_" "ERROR"; exit 1 }

$results   = @()
$startTime = Get-Date

foreach ($folderName in $SITE_MAP.Keys) {
    if ($OnlyFolder -and $folderName -ne $OnlyFolder) { continue }
    $siteId = $SITE_MAP[$folderName]
    $prefix = if ($SUBFOLDER_PREFIX.ContainsKey($folderName)) { $SUBFOLDER_PREFIX[$folderName] } else { "" }
    $result = Migrate-Folder -DattoFolder $folderName -SiteId $siteId -SitePrefix $prefix
    if ($result) { $results += $result }
}

Save-State

$elapsed   = (Get-Date) - $startTime
$totalUp   = ($results | Measure-Object -Property Uploaded -Sum).Sum
$totalSkip = ($results | Measure-Object -Property Skipped  -Sum).Sum
$totalFail = ($results | Measure-Object -Property Failed   -Sum).Sum

Write-Log ""
Write-Log "=== Summary ==="
Write-Log "Elapsed: $([math]::Round($elapsed.TotalMinutes, 1)) min"
Write-Log "Uploaded: $totalUp  Skipped: $totalSkip  Failed: $totalFail"
foreach ($r in $results) {
    Write-Log "  $($r.Folder): $($r.Uploaded) up / $($r.Skipped) skip / $($r.Failed) fail / $($r.Total) total"
}
if ($totalFail -gt 0) { Write-Log "Some failures - check log: $LOG_FILE" "WARN"; exit 1 }
Write-Log "=== Done ==="
