src/PowerShell/Scan.ps1

# cdp PowerShell domain: Scan.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function Initialize-Cdp {
    <#
    .SYNOPSIS
        Initialize cdp for first-time use.
 
    .DESCRIPTION
        Creates the default cdp config if needed, saves it as the active config,
        checks fzf availability, and optionally scans a root directory for Git
        repositories.
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$RootPath,

        [Parameter(Mandatory = $false)]
        [string]$ConfigPath,

        [Parameter(Mandatory = $false)]
        [int]$MaxDepth = 4,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
        $ConfigPath = Join-Path (Get-CdpUserHome) ".cdp\projects.json"
    }

    $initDetails = [PSCustomObject]@{
        ConfigPath = $ConfigPath
        FzfFound = $null -ne (Resolve-CdpFzfCommand)
        ScanResult = $null
    }
    if (-not $PSCmdlet.ShouldProcess($ConfigPath, 'Initialize cdp configuration and active selection')) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action 'initialize-cdp' -Target $ConfigPath -Status $status -Changed $false -Details $initDetails
        }
        return
    }

    Initialize-ConfigFile -ConfigPath $ConfigPath
    Save-ConfigChoice -ConfigPath $ConfigPath

    $fzfFound = $null -ne (Resolve-CdpFzfCommand)
    Write-CdpBrandHeader
    Write-Host "Config: $ConfigPath" -ForegroundColor Cyan
    Write-Host "Saved active config choice." -ForegroundColor Green
    if ($fzfFound) {
        Write-Host "fzf: found" -ForegroundColor Green
    } else {
        Write-Host "fzf: not found. Install with winget install fzf" -ForegroundColor Yellow
    }

    $scanResult = $null
    if (-not [string]::IsNullOrWhiteSpace($RootPath)) {
        $scanResult = Import-GitProjects -RootPath $RootPath -ConfigPath $ConfigPath -MaxDepth $MaxDepth -PassThru -Confirm:$false
    }

    if ($PassThru) {
        $initDetails.FzfFound = $fzfFound
        $initDetails.ScanResult = $scanResult
        return New-CdpActionResult -Action 'initialize-cdp' -Target $ConfigPath -Status 'succeeded' -Changed $true -Details $initDetails
    }
}

function Get-CdpComparablePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    try {
        return (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
    } catch {
        try {
            return [System.IO.Path]::GetFullPath($Path).TrimEnd('\', '/')
        } catch {
            return $Path.TrimEnd('\', '/')
        }
    }
}

function Get-CdpUniqueProjectName {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [object]$ExistingNames
    )

    $baseName = Split-Path -Leaf $Path
    if (-not $ExistingNames.Contains($baseName)) {
        return $baseName
    }

    $parentPath = Split-Path -Parent $Path
    $parentName = Split-Path -Leaf $parentPath
    $candidateRoot = if ([string]::IsNullOrWhiteSpace($parentName)) {
        $baseName
    } else {
        "$parentName-$baseName"
    }

    $candidate = $candidateRoot
    $index = 2
    while ($ExistingNames.Contains($candidate)) {
        $candidate = "$candidateRoot-$index"
        $index++
    }

    $candidate
}

function Get-CdpGitRepositoryRoots {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath,

        [Parameter(Mandatory = $false)]
        [int]$MaxDepth = 4
    )

    $resolvedRoot = (Resolve-Path -LiteralPath $RootPath -ErrorAction Stop).Path

    function Search-GitRepository {
        param(
            [string]$Path,
            [int]$Depth
        )

        if (Test-Path -LiteralPath (Join-Path $Path '.git')) {
            [PSCustomObject]@{ Path = $Path }
            return
        }

        if ($Depth -le 0) {
            return
        }

        Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ne '.git' } |
            ForEach-Object { Search-GitRepository -Path $_.FullName -Depth ($Depth - 1) }
    }

    @((Search-GitRepository -Path $resolvedRoot -Depth $MaxDepth) |
        Sort-Object -Property Path -Unique)
}

function Import-GitProjects {
    <#
    .SYNOPSIS
        Scan Git repositories and import them into the project list.
 
    .DESCRIPTION
        Finds directories containing a .git entry under the target root path and
        appends missing repositories to the active cdp configuration.
 
    .PARAMETER RootPath
        Directory to scan. Defaults to the current directory.
 
    .PARAMETER ConfigPath
        Optional custom path to projects.json file.
 
    .PARAMETER MaxDepth
        Maximum directory depth to scan under RootPath. Defaults to 4.
 
    .PARAMETER PassThru
        Returns a summary object for tests and scripting.
 
    .EXAMPLE
        cdp-scan E:\Projects
        # Imports Git repositories below E:\Projects
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $false)]
        [string]$RootPath,

        [Parameter(Mandatory = $false)]
        [string]$ConfigPath,

        [Parameter(Mandatory = $false)]
        [int]$MaxDepth = 4,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    if ([string]::IsNullOrWhiteSpace($RootPath)) {
        $RootPath = (Get-Location).Path
    }

    $resolvedRoot = Resolve-Path -LiteralPath $RootPath -ErrorAction SilentlyContinue
    if (-not $resolvedRoot) {
        Write-Host "Error: Invalid scan path." -ForegroundColor Red
        return
    }

    if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
        $ConfigPath = Get-DefaultConfigPath
    }

    try {
        $document = Read-CdpJsonArrayMutationDocument -LiteralPath $ConfigPath
        $parsedProjects = $document.Value
        $projects = @()
        if ($null -ne $parsedProjects) {
            $projects = @($parsedProjects)
        }
        $repos = @(Get-CdpGitRepositoryRoots -RootPath $resolvedRoot.Path -MaxDepth $MaxDepth)
    } catch {
        Write-Host "Error: Failed to scan or read configuration." -ForegroundColor Red
        Write-Host "Details: $($_.Exception.Message)" -ForegroundColor Gray
        return
    }

    $existingPaths = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    $existingNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($project in $projects) {
        $resolution = Resolve-CdpProjectPath -Project $project
        if (-not $resolution.ErrorCode -and -not [string]::IsNullOrWhiteSpace($resolution.ResolvedPath)) {
            [void]$existingPaths.Add((Get-CdpComparablePath -Path $resolution.ResolvedPath))
        }
        if (-not [string]::IsNullOrWhiteSpace($project.name)) {
            [void]$existingNames.Add([string]$project.name)
        }
    }

    $addedProjects = @()
    $skippedCount = 0
    foreach ($repo in $repos) {
        $repoPath = (Get-CdpComparablePath -Path $repo.Path)
        if ($existingPaths.Contains($repoPath)) {
            $skippedCount++
            continue
        }

        $name = Get-CdpUniqueProjectName -Path $repoPath -ExistingNames $existingNames
        $newProject = [PSCustomObject]@{
            name = $name
            rootPath = $repoPath
            enabled = $true
            pinned = $false
            aliases = @()
            tags = @()
            paths = New-CdpProjectPathMap -RootPath $repoPath
        }
        $projects += $newProject
        $addedProjects += $newProject
        [void]$existingPaths.Add($repoPath)
        [void]$existingNames.Add($name)
    }

    Write-Host "Git repositories found: $($repos.Count)" -ForegroundColor Cyan
    Write-Host "Projects added: $($addedProjects.Count)" -ForegroundColor Green
    Write-Host "Projects skipped: $skippedCount" -ForegroundColor Yellow
    Write-Host "Config: $ConfigPath" -ForegroundColor Gray

    $details = [PSCustomObject]@{
        RootPath = $resolvedRoot.Path
        ConfigPath = $ConfigPath
        FoundCount = $repos.Count
        AddedCount = $addedProjects.Count
        SkippedCount = $skippedCount
        AddedProjects = $addedProjects
    }
    if ($addedProjects.Count -eq 0) {
        if ($PassThru) {
            return New-CdpActionResult -Action 'scan-import' -Target $resolvedRoot.Path -Status 'skipped' -Changed $false -Details $details
        }
        return
    }

    if (-not $PSCmdlet.ShouldProcess($ConfigPath, "Import $($addedProjects.Count) repositories found under $($resolvedRoot.Path)")) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action 'scan-import' -Target $resolvedRoot.Path -Status $status -Changed $false -Details $details
        }
        return
    }

    try {
        [void](Write-CdpJsonFile -LiteralPath $ConfigPath -Value @($projects) -ExpectedFingerprint $document.Fingerprint)
    } catch {
        Write-Host "Error: Failed to import scanned projects." -ForegroundColor Red
        Write-Host "Details: $($_.Exception.Message)" -ForegroundColor Gray
        if ($PassThru) {
            return New-CdpActionResult -Action 'scan-import' -Target $resolvedRoot.Path -Status 'failed' -Changed $false -Error $_.Exception.Message -Details $details
        }
        return
    }

    if ($PassThru) {
        return New-CdpActionResult -Action 'scan-import' -Target $resolvedRoot.Path -Status 'succeeded' -Changed $true -Details $details
    }
}