param(
    [string]$Root = ''
)

Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'

$PatchRoot = Split-Path -Parent $PSScriptRoot
$Payload = Join-Path $PatchRoot 'payload'
$SavedRootFile = Join-Path $PatchRoot 'LAST_PROJECT_ROOT.txt'
$LastBackupFile = Join-Path $PatchRoot 'LAST_BACKUP.txt'
$BaseVersion = '11.0.14.1'
$TargetVersion = '11.0.14.2'

function Resolve-ProjectRoot {
    param(
        [string]$Candidate
    )

    if ([string]::IsNullOrWhiteSpace($Candidate)) {
        if (Test-Path -LiteralPath $SavedRootFile) {
            $Candidate = (Get-Content -LiteralPath $SavedRootFile -Raw).Trim()
        }
    }

    if ([string]::IsNullOrWhiteSpace($Candidate)) {
        $Candidate = 'C:\xampp\htdocs\ddb-new'
    }

    $Candidate = $Candidate.Trim()

    if (
        $Candidate.Length -ge 2 -and
        $Candidate.StartsWith('"') -and
        $Candidate.EndsWith('"')
    ) {
        $Candidate = $Candidate.Substring(1, $Candidate.Length - 2)
    }

    $VersionFile = Join-Path $Candidate 'DDB_VERSION.json'
    $AppPath = Join-Path $Candidate 'app'
    $EnvPath = Join-Path $Candidate '.env'

    $VersionExists = Test-Path -LiteralPath $VersionFile
    $AppExists = Test-Path -LiteralPath $AppPath
    $EnvExists = Test-Path -LiteralPath $EnvPath

    if ($VersionExists -and $AppExists -and $EnvExists) {
        Set-Content -LiteralPath $SavedRootFile -Value $Candidate -Encoding UTF8
        return $Candidate
    }

    throw "Invalid project root: $Candidate"
}

function Get-PhpExecutable {
    $PhpPath = 'C:\xampp\php\php.exe'

    if (-not (Test-Path -LiteralPath $PhpPath)) {
        $PhpCommand = Get-Command php -ErrorAction SilentlyContinue

        if ($null -ne $PhpCommand) {
            $PhpPath = $PhpCommand.Source
        }
    }

    if (
        [string]::IsNullOrWhiteSpace($PhpPath) -or
        -not (Test-Path -LiteralPath $PhpPath)
    ) {
        throw 'PHP executable not found.'
    }

    return $PhpPath
}

function Read-InstalledVersion {
    $VersionPath = Join-Path $script:ProjectRoot 'DDB_VERSION.json'
    $VersionText = Get-Content -LiteralPath $VersionPath -Raw
    $VersionObject = $VersionText | ConvertFrom-Json

    return [string]$VersionObject.version
}

function Ensure-Directory {
    param(
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Apply-Patch {
    $InstalledVersion = Read-InstalledVersion

    if (
        $InstalledVersion -ne $BaseVersion -and
        $InstalledVersion -ne $TargetVersion
    ) {
        throw (
            "Base mismatch. Expected $BaseVersion or $TargetVersion; " +
            "found $InstalledVersion."
        )
    }

    if (-not (Test-Path -LiteralPath $Payload)) {
        throw "Payload folder not found: $Payload"
    }

    $Stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
    $BackupRoot = Join-Path $PatchRoot ("backup_" + $Stamp)
    Ensure-Directory -Path $BackupRoot

    $CreatedFiles = New-Object System.Collections.Generic.List[string]
    $PayloadFiles = Get-ChildItem -LiteralPath $Payload -Recurse |
        Where-Object { -not $_.PSIsContainer }

    foreach ($PayloadFile in $PayloadFiles) {
        $RelativePath = $PayloadFile.FullName.Substring($Payload.Length)
        $RelativePath = $RelativePath.TrimStart([char]92, [char]47)
        $Destination = Join-Path $script:ProjectRoot $RelativePath
        $DestinationDirectory = Split-Path -Parent $Destination

        Ensure-Directory -Path $DestinationDirectory

        if (Test-Path -LiteralPath $Destination) {
            $BackupFile = Join-Path $BackupRoot $RelativePath
            $BackupDirectory = Split-Path -Parent $BackupFile
            Ensure-Directory -Path $BackupDirectory
            Copy-Item -LiteralPath $Destination -Destination $BackupFile -Force
        }
        else {
            $CreatedFiles.Add($RelativePath)
        }

        Copy-Item -LiteralPath $PayloadFile.FullName -Destination $Destination -Force
    }

    $BackupManifest = [ordered]@{
        schema = 'DDB_PATCH_BACKUP_V1'
        created_at = (Get-Date).ToString('o')
        base_version = $InstalledVersion
        target_version = $TargetVersion
        project_root = $script:ProjectRoot
        created_files = @($CreatedFiles)
    }

    $BackupManifestPath = Join-Path $BackupRoot 'BACKUP_MANIFEST.json'
    $BackupManifest |
        ConvertTo-Json -Depth 6 |
        Set-Content -LiteralPath $BackupManifestPath -Encoding UTF8

    Set-Content -LiteralPath $LastBackupFile -Value $BackupRoot -Encoding UTF8

    Write-Host "[PASS] Applied source candidate $TargetVersion"
    Write-Host '[INFO] Professional role editor installed.'
    Write-Host '[INFO] Patch Oracle writes: NONE.'
    Write-Host '[INFO] Run option 4 before using role creation or editing.'
}

function Assert-PHP7Compatibility {
    $Files = @(
        'app\Core\AccessControl.php',
        'app\Controllers\AccessControlController.php',
        'app\Repositories\AccessControlRepository.php',
        'app\Views\access_control\index.php',
        'app\Views\layouts\main.php',
        'tools\access-control-r45-p14-install.php',
        'tools\access-control-r45-p14-2-upgrade.php',
        'tools\access-control-r45-p14-2-live-acceptance.php'
    )

    foreach ($RelativePath in $Files) {
        $FilePath = Join-Path $script:ProjectRoot $RelativePath

        if (-not (Test-Path -LiteralPath $FilePath)) {
            throw "Required runtime file is missing: $RelativePath"
        }

        $Content = Get-Content -LiteralPath $FilePath -Raw

        if ($Content -match '\bmatch\s*\(') {
            throw "PHP 8 match expression detected: $RelativePath"
        }

        if ($Content.Contains('?->')) {
            throw "PHP 8 nullsafe operator detected: $RelativePath"
        }

        if ($Content -match '#\s*\[') {
            throw "PHP 8 attribute syntax detected: $RelativePath"
        }

        if ($Content -match '\bfn\s*\(') {
            throw "PHP arrow function detected: $RelativePath"
        }
    }

    Write-Host '[PASS] PHP 7.4 compatibility guard.'
}

function Run-FullTest {
    $PhpPath = Get-PhpExecutable
    $RuntimeVersion = & $PhpPath -r 'echo PHP_VERSION;'

    Write-Host ("[INFO] Runtime PHP: " + $RuntimeVersion)

    Assert-PHP7Compatibility

    $Files = @(
        'app\Core\AccessControl.php',
        'app\Controllers\AccessControlController.php',
        'app\Repositories\AccessControlRepository.php',
        'app\Views\access_control\index.php',
        'app\Views\layouts\main.php',
        'tools\access-control-r45-p14-install.php',
        'tools\access-control-r45-p14-2-upgrade.php',
        'tools\access-control-r45-p14-2-live-acceptance.php',
        'tools\test_access_control_r45_p14_2.php'
    )

    foreach ($RelativePath in $Files) {
        $FilePath = Join-Path $script:ProjectRoot $RelativePath

        if (-not (Test-Path -LiteralPath $FilePath)) {
            throw "Required test file is missing: $RelativePath"
        }

        & $PhpPath -l $FilePath

        if ($LASTEXITCODE -ne 0) {
            throw "PHP lint failed: $RelativePath"
        }
    }

    $StaticQaPath = Join-Path $script:ProjectRoot 'tools\test_access_control_r45_p14_2.php'
    & $PhpPath $StaticQaPath

    if ($LASTEXITCODE -ne 0) {
        throw 'H11.0-P14.2 static QA failed.'
    }

    Write-Host '[PASS] Full Test.'
}

function Upgrade-RoleManagementSchema {
    $PhpPath = Get-PhpExecutable
    $UpgradePath = Join-Path $script:ProjectRoot 'tools\access-control-r45-p14-2-upgrade.php'

    if (-not (Test-Path -LiteralPath $UpgradePath)) {
        throw "Schema upgrade tool not found: $UpgradePath"
    }

    & $PhpPath $UpgradePath 'njch'

    if ($LASTEXITCODE -ne 0) {
        throw 'Role Management schema upgrade failed.'
    }
}

function Run-LiveAcceptance {
    $PhpPath = Get-PhpExecutable
    $AcceptancePath = Join-Path $script:ProjectRoot 'tools\access-control-r45-p14-2-live-acceptance.php'
    $SiteAdmin = Read-Host 'Site Administrator username [7124]'

    if ([string]::IsNullOrWhiteSpace($SiteAdmin)) {
        $SiteAdmin = '7124'
    }

    if (-not (Test-Path -LiteralPath $AcceptancePath)) {
        throw "Live acceptance tool not found: $AcceptancePath"
    }

    & $PhpPath $AcceptancePath 'njch' $SiteAdmin

    if ($LASTEXITCODE -ne 0) {
        throw 'Role Management live acceptance failed.'
    }
}

function Open-AccessPage {
    $Url = 'http://localhost/public/index.php?page=access-control&lang=en#access-users'
    Start-Process $Url
}

function Open-EvidenceFolder {
    $EvidencePath = Join-Path $script:ProjectRoot 'storage\evidence\access_control'

    if (-not (Test-Path -LiteralPath $EvidencePath)) {
        throw 'No Access Control evidence folder was found.'
    }

    Invoke-Item -LiteralPath $EvidencePath
}

function Rollback-Source {
    if (-not (Test-Path -LiteralPath $LastBackupFile)) {
        throw 'No backup has been recorded.'
    }

    $BackupRoot = (Get-Content -LiteralPath $LastBackupFile -Raw).Trim()

    if (-not (Test-Path -LiteralPath $BackupRoot)) {
        throw "Backup path not found: $BackupRoot"
    }

    $ManifestPath = Join-Path $BackupRoot 'BACKUP_MANIFEST.json'
    $CreatedFiles = @()

    if (Test-Path -LiteralPath $ManifestPath) {
        $ManifestText = Get-Content -LiteralPath $ManifestPath -Raw
        $Manifest = $ManifestText | ConvertFrom-Json
        $CreatedFiles = @($Manifest.created_files)
    }

    $BackupFiles = Get-ChildItem -LiteralPath $BackupRoot -Recurse |
        Where-Object {
            -not $_.PSIsContainer -and
            $_.FullName -ne $ManifestPath
        }

    foreach ($BackupFile in $BackupFiles) {
        $RelativePath = $BackupFile.FullName.Substring($BackupRoot.Length)
        $RelativePath = $RelativePath.TrimStart([char]92, [char]47)
        $Destination = Join-Path $script:ProjectRoot $RelativePath
        $DestinationDirectory = Split-Path -Parent $Destination

        Ensure-Directory -Path $DestinationDirectory
        Copy-Item -LiteralPath $BackupFile.FullName -Destination $Destination -Force
    }

    foreach ($RelativePath in $CreatedFiles) {
        $CreatedPath = Join-Path $script:ProjectRoot ([string]$RelativePath)

        if (Test-Path -LiteralPath $CreatedPath) {
            Remove-Item -LiteralPath $CreatedPath -Force
        }
    }

    Write-Host '[PASS] Source rollback completed.'
    Write-Host '[INFO] Additive Oracle schema changes are not removed by source rollback.'
}

$script:ProjectRoot = Resolve-ProjectRoot -Candidate $Root

while ($true) {
    Clear-Host

    Write-Host '============================================================'
    Write-Host ' DDB H11.0-P14.2 Professional Role Management - Control R1'
    Write-Host '============================================================'
    Write-Host (" Project Root : " + $script:ProjectRoot)
    Write-Host (" Version      : " + (Read-InstalledVersion))
    Write-Host '------------------------------------------------------------'
    Write-Host ' [1] Apply Update'
    Write-Host ' [2] Full Test'
    Write-Host ' [3] Apply + Full Test'
    Write-Host ' [4] Upgrade Role Management Schema'
    Write-Host ' [5] Run Live Role Management Acceptance'
    Write-Host ' [6] Open Users & Roles'
    Write-Host ' [7] Open Evidence Folder'
    Write-Host ' [8] Rollback Source'
    Write-Host ' [9] Exit'

    $Choice = Read-Host 'Select'

    try {
        switch ($Choice) {
            '1' {
                Apply-Patch
            }
            '2' {
                Run-FullTest
            }
            '3' {
                Apply-Patch
                Run-FullTest
            }
            '4' {
                Upgrade-RoleManagementSchema
            }
            '5' {
                Run-LiveAcceptance
            }
            '6' {
                Open-AccessPage
            }
            '7' {
                Open-EvidenceFolder
            }
            '8' {
                Rollback-Source
            }
            '9' {
                break
            }
            default {
                Write-Host '[WARN] Invalid selection.'
            }
        }
    }
    catch {
        Write-Host ("[FAIL] " + $_.Exception.Message) -ForegroundColor Red
    }

    if ($Choice -eq '9') {
        break
    }

    Read-Host 'Press Enter'
}
