<#
.SYNOPSIS
  Lantul complet de dinaintea si din timpul unui commit: sync text VFP, curatenie,
  verificari de livrare, stare repo-uri, commit + push.

.DESCRIPTION
  1. Sincronizare, aleasa dupa starea arborelui (git status --porcelain) in AMBELE repo-uri:
     - tree curat (inceput de sesiune, nimic necomis) -> COMUN\scripts\roa_sync.ps1 -ProjectRoot
       <root>: svn update (inclusiv externalul COMUN) + git_sync (texte .??2) + commit
       "sync SVN rN" + reconciliere cu origin. Prinde aici non-fast-forward-ul cu origin (nu abia
       la push, pasul 5). Daca roa_sync.ps1 lipseste, fallback pe git_sync.ps1 (doar text, fara
       SVN), cu avertisment.
     - tree murdar (cazul normal la livrare, cu modificari necomise pe disc) -> DOAR git_sync.ps1:
       roa_sync ar face `git add -A` + commit "sync SVN" pe main peste munca in curs, inglobind-o
       intr-un commit etichetat gresit (fluxul_svn_git.md: "sync pe main doar din stare curata").
     Iesire nenula -> opreste tot. Se sare cu -FaraSync; doar partea SVN se sare cu -FaraSvn
     (roa_sync -GitOnly, aplicabil doar cand tree-ul e curat). NU ruleaza nimic in -DryRun (roa_sync
     face svn update real si poate comite/impinge) - doar anunta ce s-ar alege si de ce.
  2. curatenie.ps1 -ProjectRoot <root>         - sterge artefactele de lucru (neurmarite de git).
  3. Verificari de livrare (avertismente, neblocante): changelog neatins, fisiere de runda
     ramase, binare VFP fara perechea text in modificari, fisiere ce urmeaza sa intre in git
     dar sunt necunoscute SVN-ului ("?" la svn status - munca reala trebuie sa treaca intai
     prin SVN, vezi fluxul_svn_git.md).
  4. Stare repo proiect si repo COMUN (separat, partajat de toate aplicatiile ROA): git fetch +
     compara HEAD cu origin/<ramura> (semnaleaza daca ramura locala e in urma), apoi
     git status --short + git diff --stat.
  5. Commit + push. Proiectul implicit; COMUN doar cu -Comun explicit (altfel doar raportat).
     Push respins non-fast-forward (frecvent pe COMUN, comun tuturor aplicatiilor ROA): fetch +
     rebase peste origin + reincercare o singura data. Conflicte la rebase -> rebase --abort,
     mesaj clar, exit nenul (nu lasa repo-ul in rebase in curs). Niciodata push --force
     (nici --force-with-lease). Comite pe ramura curenta, nu creeaza alta.
  6. -DryRun: ruleaza pasii 2-4 (curatenia in -DryRun; pasul 1 doar anuntat, nu executat) si se
     opreste inainte de commit.

.PARAMETER ProjectRoot
  Radacina proiectului (implicit dedus, ca la curatenie.ps1 - doua nivele deasupra acestui script).

.PARAMETER Mesaj
  Mesajul de commit. Obligatoriu la o rulare reala (fara -DryRun).

.PARAMETER Comun
  Comite si impinge si repo-ul COMUN (implicit doar proiectul).

.PARAMETER FaraSync
  Sare peste tot pasul 1 (roa_sync/git_sync) - util cand nu s-a atins niciun binar VFP si nici
  SVN-ul nu trebuie adus la zi.

.PARAMETER FaraSvn
  Cand tree-ul e curat (altfel se ruleaza oricum doar git_sync): ruleaza roa_sync.ps1 -GitOnly,
  fara svn update si fara regenerarea textelor, doar commit sync + reconciliere origin +
  inchidere branch-uri claude/*. Util cand SVN nu e disponibil sau nu s-a atins nimic in SVN.
  Ignorat daca roa_sync.ps1 lipseste (fallback-ul pe git_sync.ps1 nu are oricum pas SVN).

.PARAMETER FaraPush
  Doar commit local, fara push.

.PARAMETER DryRun
  Nu modifica nimic - arata ce s-ar comite si unde. Pasul 1 nu se executa (roa_sync ar face svn
  update real si poate comite/impinge) - doar anunta ce s-ar alege (roa_sync sau git_sync) si de ce.

.EXAMPLE
  powershell -File COMUN\utile\livrare.ps1 -DryRun

.EXAMPLE
  powershell -File COMUN\utile\livrare.ps1 -Mesaj "achizitie import: rebindare ControlSource"

.EXAMPLE
  powershell -File COMUN\utile\livrare.ps1 -Mesaj "conventie noua in COMUN" -Comun

.EXAMPLE
  powershell -File COMUN\utile\livrare.ps1 -Mesaj "doar .prg, fara binare" -FaraSync -FaraPush

.EXAMPLE
  powershell -File COMUN\utile\livrare.ps1 -Mesaj "SVN indisponibil acum" -FaraSvn
#>
param(
    [string]$ProjectRoot,
    [string]$Mesaj,
    [switch]$Comun,
    [switch]$FaraSync,
    [switch]$FaraSvn,
    [switch]$FaraPush,
    [switch]$DryRun
)

$ErrorActionPreference = 'Stop'

if (-not $ProjectRoot) { $ProjectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent }
$comunPath = Join-Path $ProjectRoot 'COMUN'
$roaSync = Join-Path $ProjectRoot 'COMUN\scripts\roa_sync.ps1'
$gitSync = 'D:\ROA\UTIL\foxbin2prg\git_sync.ps1'
$curatenie = Join-Path $PSScriptRoot 'curatenie.ps1'

if (-not $DryRun -and -not $Mesaj) {
    Write-Output 'livrare: -Mesaj este obligatoriu (sau ruleaza cu -DryRun)'
    exit 1
}

Write-Output ('livrare: ProjectRoot=' + $ProjectRoot)

# --- tree curat in ambele repo-uri? roa_sync e unealta de inceput de sesiune (stare curata),
# nu de livrare cu modificari necomise pe disc (fluxul_svn_git.md) ---
function Test-TreeCurat($repoPath) {
    if (-not (Test-Path (Join-Path $repoPath '.git'))) { return $true }
    return (@(& git -C $repoPath status --porcelain).Count -eq 0)
}
$treeCurat = (Test-TreeCurat $ProjectRoot) -and (Test-TreeCurat $comunPath)

function Ruleaza-GitSync {
    if (-not (Test-Path $gitSync)) {
        Write-Output ('[1] git_sync: NU s-a gasit ' + $gitSync)
        exit 2
    }
    & powershell -ExecutionPolicy Bypass -File $gitSync -ProjectRoot $ProjectRoot
    if ($LASTEXITCODE -ne 0) {
        Write-Output ('[1] git_sync: ESUAT (cod ' + $LASTEXITCODE + ') - opresc tot, nu se comite')
        exit 2
    }
    Write-Output '[1] git_sync: OK'
}

# --- pas 1: tree curat -> roa_sync (fallback git_sync daca lipseste); tree murdar -> doar git_sync ---
if ($FaraSync) {
    Write-Output '[1] sincronizare: sarita (-FaraSync)'
} elseif ($DryRun) {
    # roa_sync face svn update real si poate comite/impinge (chiar si cu -GitOnly) - nu ruleaza in -DryRun.
    if (-not $treeCurat) {
        Write-Output ('[1] sincronizare: sarita (-DryRun) - modificari necomise, ar rula doar ' + $gitSync + ' (roa_sync ar ingloba munca in curs intr-un commit "sync SVN")')
    } elseif (Test-Path $roaSync) {
        $variantaGO = if ($FaraSvn) { ' -GitOnly' } else { '' }
        Write-Output ('[1] sincronizare: sarita (-DryRun) - tree curat, ar rula ' + $roaSync + $variantaGO)
    } else {
        Write-Output ('[1] sincronizare: sarita (-DryRun) - tree curat, roa_sync.ps1 lipseste, ar rula fallback ' + $gitSync)
    }
} elseif (-not $treeCurat) {
    Write-Output '[1] sincronizare: modificari necomise -> rulez doar git_sync (roa_sync ar ingloba munca in curs intr-un commit "sync SVN")'
    Ruleaza-GitSync
} elseif (Test-Path $roaSync) {
    $rsArgs = @('-ProjectRoot', $ProjectRoot)
    if ($FaraSvn) { $rsArgs += '-GitOnly' }
    & powershell -ExecutionPolicy Bypass -File $roaSync @rsArgs
    if ($LASTEXITCODE -ne 0) {
        Write-Output ('[1] roa_sync: ESUAT (cod ' + $LASTEXITCODE + ') - opresc tot, nu se comite')
        exit 2
    }
    Write-Output '[1] roa_sync: OK'
} else {
    Write-Output ('[1] roa_sync: NU s-a gasit (' + $roaSync + ') - fallback pe git_sync.ps1')
    Ruleaza-GitSync
}

# --- pas 2: curatenie ---
$curArgs = @('-ProjectRoot', $ProjectRoot)
if ($DryRun) { $curArgs += '-DryRun' }
Write-Output '[2] curatenie:'
& powershell -ExecutionPolicy Bypass -File $curatenie @curArgs | ForEach-Object { Write-Output ('      ' + $_) }
if ($LASTEXITCODE -ne 0) {
    Write-Output ('[2] curatenie: cod neasteptat ' + $LASTEXITCODE + ' (continui)')
}

# --- fisiere modificate per repo (dupa curatenie) ---
function Get-ChangedFiles($repoPath) {
    if (-not (Test-Path (Join-Path $repoPath '.git'))) { return @() }
    $rezultat = New-Object System.Collections.ArrayList
    foreach ($linie in (& git -C $repoPath status --porcelain --untracked-files=all)) {
        if (-not $linie) { continue }
        $cale = $linie.Substring(3)
        if ($cale -match ' -> ') { $cale = ($cale -split ' -> ')[1] }
        $cale = $cale.Trim('"')
        [void]$rezultat.Add($cale)
    }
    return $rezultat
}

$modifProiect = Get-ChangedFiles $ProjectRoot
$modifComun = Get-ChangedFiles $comunPath

# --- pas 3: verificari de livrare (avertismente, neblocante) ---
$avertismente = New-Object System.Collections.ArrayList

# fisiere din $modif care apar cu "?" (necunoscut) la svn status - munca reala trebuie sa treaca
# intai prin SVN (fluxul_svn_git.md). Fara svn sau folder ce nu e working copy -> tacere.
function Get-SvnNeurmarite($repoPath, $modif) {
    if ($modif.Count -eq 0) { return @() }
    if (-not (Get-Command svn -ErrorAction SilentlyContinue)) { return @() }
    $ErrorActionPreference = 'Continue'
    $iesire = & svn status $repoPath 2>$null
    if ($LASTEXITCODE -ne 0) { return @() }
    $necunoscute = New-Object System.Collections.Generic.HashSet[string] ([StringComparer]::OrdinalIgnoreCase)
    foreach ($linie in $iesire) {
        if ($linie -match '^\?\s+(.+)$') {
            $cale = $Matches[1].Trim()
            if ($cale.StartsWith($repoPath, [System.StringComparison]::OrdinalIgnoreCase)) {
                $cale = $cale.Substring($repoPath.Length).TrimStart('\', '/')
            }
            [void]$necunoscute.Add(($cale -replace '\\', '/'))
        }
    }
    return @($modif | Where-Object { $necunoscute.Contains($_) })
}

function Verifica-Livrare($modif, $repoPath, $eticheta) {
    $codExt = @('.prg', '.vc2', '.sc2', '.ps1')
    $areCod = $modif | Where-Object { $ext = [System.IO.Path]::GetExtension($_).ToLower(); $codExt -contains $ext }
    $areChangelog = $modif | Where-Object { $_ -match 'changelog.*\.txt$' }
    if ($areCod -and -not $areChangelog -and $eticheta -eq 'proiect') {
        [void]$script:avertismente.Add('changelog neatins (' + $eticheta + ') desi s-a modificat cod')
    }

    foreach ($f in $modif) {
        $ext = [System.IO.Path]::GetExtension($f).ToLower()
        if ($ext -eq '.vcx') { $pereche = [System.IO.Path]::ChangeExtension($f, '.vc2') }
        elseif ($ext -eq '.scx') { $pereche = [System.IO.Path]::ChangeExtension($f, '.sc2') }
        else { continue }
        if ($modif -notcontains $pereche) {
            [void]$script:avertismente.Add('binar fara text sincronizat in modificari (' + $eticheta + '): ' + $f)
        }
    }

    $docsPath = Join-Path $repoPath 'docs'
    if (Test-Path $docsPath) {
        $ramase = Get-ChildItem -Path $docsPath -Include @('propuneri_*.md', 'handoff*.md', 'runda*.md') -Recurse -ErrorAction SilentlyContinue
        foreach ($r in $ramase) {
            [void]$script:avertismente.Add('fisier de runda ramas (' + $eticheta + '): docs\' + $r.Name)
        }
    }

    $neurmarite = Get-SvnNeurmarite $repoPath $modif
    foreach ($f in $neurmarite) {
        [void]$script:avertismente.Add('fisier necunoscut SVN (' + $eticheta + '), fa svn add + svn commit intai: ' + $f)
    }
}

Verifica-Livrare $modifProiect $ProjectRoot 'proiect'
Verifica-Livrare $modifComun $comunPath 'COMUN'

Write-Output '[3] verificari de livrare:'
if ($avertismente.Count -eq 0) {
    Write-Output '      fara avertismente'
} else {
    foreach ($a in $avertismente) { Write-Output ('      ATENTIE: ' + $a) }
}

# --- ramura locala vs origin (fetch + numarare ahead/behind) ---
function Get-StareFetch($repoPath) {
    # ErrorActionPreference local 'Continue': altfel 2>$null pe un exe extern (chiar la iesire 0,
    # git scrie mesaje normale pe stderr) devine NativeCommandError terminal sub preferinta 'Stop'.
    $ErrorActionPreference = 'Continue'
    if (-not (Test-Path (Join-Path $repoPath '.git'))) { return $null }
    $remote = & git -C $repoPath remote
    if (-not $remote) { return @{ AreRemote = $false } }
    & git -C $repoPath fetch --quiet 2>$null | Out-Null
    $fetchOk = ($LASTEXITCODE -eq 0)
    $upstream = (& git -C $repoPath rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>$null)
    if ($LASTEXITCODE -ne 0 -or -not $upstream) {
        return @{ AreRemote = $true; FetchOk = $fetchOk; AreUpstream = $false }
    }
    $numere = (& git -C $repoPath rev-list --left-right --count "$upstream...HEAD" 2>$null) -split '\s+' | Where-Object { $_ -ne '' }
    return @{ AreRemote = $true; FetchOk = $fetchOk; AreUpstream = $true; Upstream = $upstream; Behind = [int]$numere[0]; Ahead = [int]$numere[1] }
}

function Scrie-StareFetch($stare) {
    if (-not $stare) { return }
    if (-not $stare.AreRemote) { return }
    if (-not $stare.FetchOk) { Write-Output '      (fetch esuat, sar verificarea fata de origin)'; return }
    if (-not $stare.AreUpstream) { return }
    if ($stare.Behind -gt 0) {
        Write-Output ('      ramura locala e in urma cu ' + $stare.Behind + ' commit-uri fata de ' + $stare.Upstream)
    }
}

# --- pas 4: stare repo-uri ---
Write-Output '[4] stare repo proiect:'
$stareProiect = Get-StareFetch $ProjectRoot
Scrie-StareFetch $stareProiect
& git -C $ProjectRoot status --short | ForEach-Object { Write-Output ('      ' + $_) }
& git -C $ProjectRoot diff --stat HEAD | ForEach-Object { Write-Output ('      ' + $_) }

Write-Output '[4] stare repo COMUN:'
if (Test-Path (Join-Path $comunPath '.git')) {
    $stareComun = Get-StareFetch $comunPath
    Scrie-StareFetch $stareComun
    & git -C $comunPath status --short | ForEach-Object { Write-Output ('      ' + $_) }
    & git -C $comunPath diff --stat HEAD | ForEach-Object { Write-Output ('      ' + $_) }
} else {
    Write-Output '      (fara repo git in COMUN)'
}

if ($DryRun) {
    Write-Output '[5] commit/push: sarit (-DryRun)'
    if ($modifProiect.Count -gt 0) {
        Write-Output ('      ar comite in proiect (' + $modifProiect.Count + ' fisiere)')
    } else {
        Write-Output '      proiect: nimic de comis'
    }
    if ($modifComun.Count -gt 0) {
        if ($Comun) {
            Write-Output ('      ar comite in COMUN (' + $modifComun.Count + ' fisiere)')
        } else {
            Write-Output ('      COMUN are ' + $modifComun.Count + ' fisiere necomise - nu se comite fara -Comun')
        }
    } else {
        Write-Output '      COMUN: nimic de comis'
    }
    exit 0
}

# --- pas 5: commit + push ---
$exitCode = 0

# push, si daca e respins non-fast-forward: fetch + rebase peste upstream + o singura reincercare.
# Conflicte la rebase -> rebase --abort (nu lasa repo-ul in rebase in curs) + esec, fara push --force.
function Invoke-PushCuRebaseRetry($repoPath, $eticheta) {
    # ErrorActionPreference local 'Continue': vezi motivul in Get-StareFetch.
    $ErrorActionPreference = 'Continue'
    # | Out-Null pe fiecare apel git: stdout-ul lui n-ar trebui sa ajunga in valoarea
    # intoarsa de functie (rezultatul e capturat de apelant) - vezi Comite-Si-Impinge.
    & git -C $repoPath push | Out-Null
    if ($LASTEXITCODE -eq 0) {
        Write-Host ('[5] ' + $eticheta + ': push OK')
        return 0
    }
    & git -C $repoPath fetch --quiet 2>$null | Out-Null
    $upstream = (& git -C $repoPath rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>$null)
    if ($LASTEXITCODE -ne 0 -or -not $upstream) {
        Write-Host ('[5] ' + $eticheta + ': PUSH ESUAT (fara upstream de comparat)')
        return 1
    }
    $numere = (& git -C $repoPath rev-list --left-right --count "$upstream...HEAD" 2>$null) -split '\s+' | Where-Object { $_ -ne '' }
    $behind = [int]$numere[0]
    if ($behind -eq 0) {
        Write-Host ('[5] ' + $eticheta + ': PUSH ESUAT (nu e in urma fata de ' + $upstream + ', alta cauza)')
        return 1
    }
    Write-Host ('[5] ' + $eticheta + ': push respins, ramura in urma cu ' + $behind + ' fata de ' + $upstream + ' - rebase')
    & git -C $repoPath rebase $upstream | Out-Null
    if ($LASTEXITCODE -ne 0) {
        & git -C $repoPath rebase --abort 2>$null | Out-Null
        Write-Host ('[5] ' + $eticheta + ': REBASE CU CONFLICTE - abort facut, e nevoie de interventie manuala')
        return 1
    }
    Write-Host ('[5] ' + $eticheta + ': rebase OK, reincerc push')
    & git -C $repoPath push | Out-Null
    if ($LASTEXITCODE -ne 0) {
        Write-Host ('[5] ' + $eticheta + ': PUSH ESUAT si dupa rebase')
        return 1
    }
    Write-Host ('[5] ' + $eticheta + ': push OK dupa rebase')
    return 0
}

function Comite-Si-Impinge($repoPath, $eticheta, $faraPush, $mesajFile) {
    # mesajele folosesc Write-Host (nu Write-Output): functia e apelata cu rezultatul
    # capturat intr-o variabila, iar Write-Output ar ajunge in valoarea returnata.
    if (-not (Test-Path (Join-Path $repoPath '.git'))) {
        Write-Host ('[5] ' + $eticheta + ': fara repo git, sarit')
        return 0
    }
    & git -C $repoPath add -A | Out-Null
    & git -C $repoPath diff --cached --quiet
    if ($LASTEXITCODE -eq 0) {
        Write-Host ('[5] ' + $eticheta + ': nimic de comis')
        return 0
    }
    $ramura = (& git -C $repoPath rev-parse --abbrev-ref HEAD)
    # -F <fisier>, nu -m $Mesaj: PowerShell 5.1 nu escapeaza ghilimele/backtick/$ cand paseaza
    # argumente catre un exe nativ - un mesaj cu ghilimele duble ("sync SVN") sparge -m $Mesaj.
    & git -C $repoPath commit -F $mesajFile | Out-Null
    if ($LASTEXITCODE -ne 0) {
        Write-Host ('[5] ' + $eticheta + ': COMMIT ESUAT pe ramura ' + $ramura)
        return 1
    }
    Write-Host ('[5] ' + $eticheta + ': commit OK pe ramura ' + $ramura)
    if ($faraPush) {
        Write-Host ('[5] ' + $eticheta + ': push sarit (-FaraPush)')
        return 0
    }
    return (Invoke-PushCuRebaseRetry $repoPath $eticheta)
}

# mesajul de commit trece printr-un fisier temporar (nu ca argument -m): singurul mod sigur
# de a duce prin PowerShell->exe nativ un text cu ghilimele/backtick/$/linii multiple/"-" la
# inceput de linie fara sa fie interpretat sau trunchiat. Sters mereu, inclusiv pe esec.
$mesajDir = $null
try {
    $mesajDir = Join-Path ([System.IO.Path]::GetTempPath()) ('livrare_' + [guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $mesajDir | Out-Null
    $mesajFile = Join-Path $mesajDir 'mesaj_commit.txt'
    # UTF8Encoding($false): fara BOM - Out-File -Encoding utf8 scrie BOM in PS 5.1, iar git
    # il baga in mesajul de commit ca prim caracter (U+FEFF) daca-l citim cu -F.
    [System.IO.File]::WriteAllText($mesajFile, $Mesaj, (New-Object System.Text.UTF8Encoding $false))

    $exitCode += (Comite-Si-Impinge $ProjectRoot 'proiect' $FaraPush $mesajFile)

    if ($Comun) {
        $exitCode += (Comite-Si-Impinge $comunPath 'COMUN' $FaraPush $mesajFile)
    } elseif ($modifComun.Count -gt 0) {
        Write-Output ('[5] COMUN: ' + $modifComun.Count + ' fisiere necomise - nu se comite fara -Comun')
    }
} finally {
    if ($mesajDir -and (Test-Path $mesajDir)) { Remove-Item -LiteralPath $mesajDir -Recurse -Force -ErrorAction SilentlyContinue }
}

if ($exitCode -gt 0) { exit 3 }
Write-Output 'livrare: OK'
exit 0
