# publicare_scripturi.ps1 - publica scripturile de migrare .sql catre clientii ROA.
# Doi pasi, exact ca formularul din tasks.exe (clase\generare_script.vcx):
#   1. import  - scrie fiecare .sql in CONTAFIN_ORACLE.UPD_DATABASE de pe ROA_CENTRAL
#                (script_order = 0, un rand = un script intreg); daca script_name exista deja,
#                face update pe id, ca sa se republice continutul corectat;
#   2. arhiva  - regenereaza database_<luna>.zip / databasen_<luna>.zip si indexii
#                roa_database.xml / roa_databasen.xml in Y:\ROAUPDATE\_UPDATE.
# Continutul din arhiva se citeste INAPOI din baza, nu de pe disc, si se compara octet cu octet cu
# fisierul sursa - deci arhiva contine exact ce vor primi clientii. Optional (implicit pornit),
# arhiva generata se citeste inapoi prin zip_util_pkg, cititorul Oracle al clientului.
#
# Configurarea se citeste din settings.ini-ul lui tasks.exe (aceleasi chei pe care le foloseste
# formularul): [connection] host_database/username_database/password_database, [folder]
# script_folder (\SCRIPTURI\ -> \SCRIPTURI_CLAR\), roa_output (+ _UPDATE) si sqlplus_exe, [script]
# prefixele valide. Aliasul TNS se ia din DSN-ul ODBC numit in host_database (cheia ServerName din
# registri). Orice parametru dat explicit are prioritate fata de ini.
#
# Utilizare:
#   powershell -File COMUN\utile\publicare_scripturi.ps1 -DryRun
#   powershell -File COMUN\utile\publicare_scripturi.ps1 -Fisier a.sql,b.sql
#   powershell -File COMUN\utile\publicare_scripturi.ps1 -Luna 2026-08
#   powershell -File COMUN\utile\publicare_scripturi.ps1 -Luna 2026-08 -DoarArhiva
param(
    [string[]]$Fisier,
    [string]$Luna,
    [string]$Settings = 'D:\PROIECTE\fox\tasks\settings.ini',
    [string]$DirScripturi,
    [string]$ZipPath,
    [string]$BackupPath,
    [switch]$DoarImport,
    [switch]$DoarArhiva,
    [switch]$Toate,
    [switch]$FaraVerificareZip,
    [switch]$DryRun,
    [switch]$PastreazaTemp,
    [string]$Alias,
    [string]$User,
    [string]$Password,
    [string]$SqlPlus
)

$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem

# octeti per bucata trimisa/citita ca hexazecimal (2 caractere per octet, sub limita de linie sqlplus)
$BUCATA = 900
# bucati per bloc PL/SQL (sursa unui bloc trebuie sa ramana sub ~64k)
$BUCATI_PER_BLOC = 15

function Scrie-Titlu {
    param([string]$Text)
    Write-Output ''
    Write-Output ('===== ' + $Text + ' =====')
}

function Opreste {
    param([string]$Mesaj)
    Write-Output ''
    Write-Output ('EROARE: ' + $Mesaj)
    Curata-Temp
    exit 1
}

function Curata-Temp {
    if ($PastreazaTemp) {
        if (Test-Path $TempDir) { Write-Output ('Temp pastrat: ' + $TempDir) }
        return
    }
    if (Test-Path $TempDir) { Remove-Item -LiteralPath $TempDir -Recurse -Force -ErrorAction SilentlyContinue }
}

function Invoke-SqlPlus {
    # ruleaza un script sqlplus; intoarce textul si codul de iesire (whenever sqlerror => cod <> 0)
    param([string]$Sql, [string]$Nume = 'q')
    $f = Join-Path $TempDir ($Nume + '_' + [guid]::NewGuid().ToString('N') + '.sql')
    $antet = "whenever sqlerror exit failure rollback`r`nwhenever oserror exit failure`r`nset define off`r`nset feedback off`r`n"
    [System.IO.File]::WriteAllText($f, ($antet + $Sql), [System.Text.Encoding]::ASCII)
    try {
        $out = & $SqlPlus -L -S $ConnStr ('@' + $f) 2>&1
        $cod = $LASTEXITCODE
    } catch {
        $out = 'EROARE lansare sqlplus: ' + $_.Exception.Message
        $cod = -1
    }
    if (-not $PastreazaTemp) { Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue }
    $text = ($out | ForEach-Object { [string]$_ }) -join "`n"
    [PSCustomObject]@{ Text = $text; Cod = $cod }
}

function Test-Rezultat {
    param($R, [string]$Context)
    if ($R.Cod -ne 0 -or $R.Text -match 'ORA-\d{5}|SP2-\d{4}|PLS-\d{5}') {
        Write-Output $R.Text
        Opreste ($Context + ' - sqlplus a raportat eroare (cod ' + $R.Cod + ').')
    }
}

function ConvertTo-Hex {
    param([byte[]]$Octeti)
    if ($Octeti.Length -eq 0) { return '' }
    [System.BitConverter]::ToString($Octeti).Replace('-', '')
}

function ConvertFrom-Hex {
    param([string]$Hex)
    $n = [int]($Hex.Length / 2)
    $b = New-Object byte[] $n
    for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($Hex.Substring($i * 2, 2), 16) }
    ,$b
}

function Citeste-Ini {
    param([string]$Cale)
    $ini = @{}
    $sectiune = ''
    foreach ($linie in [System.IO.File]::ReadAllLines($Cale, [System.Text.Encoding]::GetEncoding(1252))) {
        $linie = $linie.Trim()
        if (-not $linie -or $linie.StartsWith(';') -or $linie.StartsWith('#')) { continue }
        if ($linie -match '^\[(.+)\]$') {
            $sectiune = $Matches[1].ToLower()
            if (-not $ini.ContainsKey($sectiune)) { $ini[$sectiune] = @{} }
            continue
        }
        $p = $linie.IndexOf('=')
        if ($p -lt 1 -or -not $sectiune) { continue }
        $ini[$sectiune][$linie.Substring(0, $p).Trim().ToLower()] = $linie.Substring($p + 1).Trim()
    }
    $ini
}

function Ia-Cheie {
    param($Ini, [string]$Sectiune, [string]$Cheie)
    $s = $Sectiune.ToLower()
    $c = $Cheie.ToLower()
    if ($Ini.ContainsKey($s) -and $Ini[$s].ContainsKey($c)) { return $Ini[$s][$c] }
    ''
}

function Rezolva-Alias {
    # numele din host_database e un DSN ODBC; aliasul TNS e in cheia ServerName a DSN-ului
    param([string]$Dsn)
    foreach ($radacina in @('HKCU:\SOFTWARE\ODBC\ODBC.INI', 'HKLM:\SOFTWARE\ODBC\ODBC.INI', 'HKLM:\SOFTWARE\WOW6432Node\ODBC\ODBC.INI')) {
        $cale = Join-Path $radacina $Dsn
        if (Test-Path $cale) {
            $v = (Get-ItemProperty -Path $cale -Name 'ServerName' -ErrorAction SilentlyContinue).ServerName
            if ($v) { return $v }
        }
    }
    # nu e DSN ODBC - il tratam ca alias TNS direct
    $Dsn
}

function Compara-Octeti {
    param([byte[]]$A, [byte[]]$B)
    if ($null -eq $A -or $null -eq $B) { return $false }
    if ($A.Length -ne $B.Length) { return $false }
    for ($i = 0; $i -lt $A.Length; $i++) { if ($A[$i] -ne $B[$i]) { return $false } }
    $true
}

function Split-Hex {
    # taie un sir hexazecimal in bucati de $BUCATA octeti
    param([string]$Hex)
    $l = New-Object System.Collections.ArrayList
    $pas = $BUCATA * 2
    for ($i = 0; $i -lt $Hex.Length; $i += $pas) {
        [void]$l.Add($Hex.Substring($i, [Math]::Min($pas, $Hex.Length - $i)))
    }
    ,$l
}

function Parseaza-NumeScript {
    # <prefix>_YYYY_MM_DD_NN_<tip>.sql -> obiect cu elementele componente (ca parsescriptname din VFP)
    param([string]$Cale)
    $stem = [System.IO.Path]::GetFileNameWithoutExtension($Cale)
    if ($stem -notmatch '^([A-Za-z]+)_(\d{4})_(\d{2})_(\d{2})_(\d+)(?:_(.+))?$') { return $null }
    $an = [int]$Matches[2]; $luna = [int]$Matches[3]; $zi = [int]$Matches[4]
    try { $data = Get-Date -Year $an -Month $luna -Day $zi -Hour 0 -Minute 0 -Second 0 } catch { return $null }
    $tip = ''
    if ($Matches[6]) { $tip = $Matches[6].ToUpper() }
    [PSCustomObject]@{
        Cale       = $Cale
        NumeScript = ($stem.ToUpper() + '.SQL')
        Stem       = $stem.ToUpper()
        Prefix     = $Matches[1].ToUpper()
        Data       = $data
        DataYmd    = $data.ToString('yyyyMMdd')
        Secventa   = [int]$Matches[5]
        Tip        = $tip
    }
}

function Citeste-MetadateLuna {
    # metadatele scripturilor intregi (script_order = 0) din luna, in ordinea din arhiva
    param([datetime]$Inceput, [datetime]$Sfarsit)
    $sql = @"
set pagesize 0 linesize 600 trimspool on heading off
select 'M|' || id || '|' || script_name || '|' || script_type || '|' || to_char(script_date,'yyyymmdd') ||
       '|' || to_char(script_seq) || '|' || to_char(script_order) || '|' || nvl(script_appver,'') ||
       '|' || nvl(length(script_content),0)
  from upd_database
 where script_order = 0
   and script_date between to_date('$($Inceput.ToString('yyyyMMdd'))','yyyymmdd') and to_date('$($Sfarsit.ToString('yyyyMMdd'))','yyyymmdd')
 order by script_name, id;
exit
"@
    $r = Invoke-SqlPlus -Sql $sql -Nume 'meta'
    Test-Rezultat -R $r -Context 'citire metadate din UPD_DATABASE'
    $l = New-Object System.Collections.ArrayList
    foreach ($linie in ($r.Text -split "`r?`n")) {
        $linie = $linie.Trim()
        if (-not $linie.StartsWith('M|')) { continue }
        $c = $linie.Substring(2) -split '\|'
        if ($c.Count -lt 8) { continue }
        [void]$l.Add([PSCustomObject]@{
            Id         = [int]$c[0]
            NumeScript = $c[1].Trim()
            Stem       = [System.IO.Path]::GetFileNameWithoutExtension($c[1].Trim())
            Tip        = $c[2].Trim()
            DataYmd    = $c[3].Trim()
            Secventa   = [int]$c[4]
            Ordine     = [int]$c[5]
            AppVer     = $c[6].Trim()
            Lungime    = [int]$c[7]
        })
    }
    ,$l
}

function Citeste-Continut {
    # citeste inapoi CLOB-urile ca octeti CP1252 (hexazecimal), pentru toate id-urile date
    param($Metadate)
    $rezultat = @{}
    if ($Metadate.Count -eq 0) { return $rezultat }
    $maxBucati = 1
    foreach ($m in $Metadate) {
        $rezultat[$m.Id] = New-Object System.Text.StringBuilder
        $b = [Math]::Ceiling($m.Lungime / $BUCATA)
        if ($b -gt $maxBucati) { $maxBucati = [int]$b }
    }
    $toateId = @($Metadate | ForEach-Object { $_.Id })
    $grupuri = New-Object System.Collections.ArrayList
    for ($i = 0; $i -lt $toateId.Count; $i += 500) {
        [void]$grupuri.Add('c.id in (' + (($toateId[$i..([Math]::Min($i + 499, $toateId.Count - 1))]) -join ',') + ')')
    }
    $ids = $grupuri -join ' or '
    $sql = @"
set pagesize 0 linesize 32767 long 2000000000 longchunksize 32767 trimspool on heading off arraysize 100
select '#' || c.id || '#' || lpad(to_char(l.n),7,'0') || '#' ||
       rawtohex(utl_i18n.string_to_raw(dbms_lob.substr(c.script_content, $BUCATA, (l.n-1)*$BUCATA+1), 'WE8MSWIN1252'))
  from upd_database c,
       (select level n from dual connect by level <= $maxBucati) l
 where ($ids)
   and (l.n-1)*$BUCATA < nvl(length(c.script_content),0)
 order by c.id, l.n;
exit
"@
    $r = Invoke-SqlPlus -Sql $sql -Nume 'citire'
    Test-Rezultat -R $r -Context 'citire continut scripturi din UPD_DATABASE'
    foreach ($linie in ($r.Text -split "`r?`n")) {
        $linie = $linie.Trim()
        if (-not $linie.StartsWith('#')) { continue }
        $c = $linie.Substring(1) -split '#', 3
        if ($c.Count -lt 3) { continue }
        $id = [int]$c[0]
        if ($rezultat.ContainsKey($id)) { [void]$rezultat[$id].Append($c[2]) }
    }
    $final = @{}
    foreach ($k in $rezultat.Keys) { $final[$k] = ConvertFrom-Hex $rezultat[$k].ToString() }
    $final
}

function Importa-Script {
    # insert sau update pe script_name existent; continutul se trimite hexazecimal, bucata cu bucata
    param($Info, [byte[]]$Octeti)
    $hex = ConvertTo-Hex $Octeti
    $bucati = Split-Hex $hex
    $sb = New-Object System.Text.StringBuilder
    [void]$sb.AppendLine('set serveroutput on size 1000000')
    [void]$sb.AppendLine('variable b_id number')
    [void]$sb.AppendLine('declare')
    [void]$sb.AppendLine('  l_id number;')
    [void]$sb.AppendLine('begin')
    [void]$sb.AppendLine("  select max(id) into l_id from upd_database where script_name = '$($Info.NumeScript)';")
    [void]$sb.AppendLine('  if l_id is null then')
    [void]$sb.AppendLine('    insert into upd_database')
    [void]$sb.AppendLine('      (script_name, script_type, script_date, script_seq, script_content, script_order, script_appver)')
    [void]$sb.AppendLine("    values ('$($Info.NumeScript)', '$($Info.Tip)', to_date('$($Info.DataYmd)','yyyymmdd'), $($Info.Secventa), empty_clob(), 0, null)")
    [void]$sb.AppendLine('    returning id into l_id;')
    [void]$sb.AppendLine('  else')
    [void]$sb.AppendLine('    update upd_database')
    [void]$sb.AppendLine("       set script_type = '$($Info.Tip)',")
    [void]$sb.AppendLine("           script_date = to_date('$($Info.DataYmd)','yyyymmdd'),")
    [void]$sb.AppendLine("           script_seq = $($Info.Secventa),")
    [void]$sb.AppendLine('           script_content = empty_clob(),')
    [void]$sb.AppendLine('           script_order = 0,')
    [void]$sb.AppendLine('           script_appver = null')
    [void]$sb.AppendLine('     where id = l_id;')
    [void]$sb.AppendLine('  end if;')
    [void]$sb.AppendLine('  :b_id := l_id;')
    [void]$sb.AppendLine('  commit;')
    [void]$sb.AppendLine("  dbms_output.put_line('IDSCRIPT=' || l_id);")
    [void]$sb.AppendLine('end;')
    [void]$sb.AppendLine('/')

    for ($i = 0; $i -lt $bucati.Count; $i += $BUCATI_PER_BLOC) {
        [void]$sb.AppendLine('declare s varchar2(32767); l clob;')
        [void]$sb.AppendLine('begin')
        [void]$sb.AppendLine('  select script_content into l from upd_database where id = :b_id for update;')
        for ($j = $i; $j -lt [Math]::Min($i + $BUCATI_PER_BLOC, $bucati.Count); $j++) {
            [void]$sb.AppendLine("  s := utl_i18n.raw_to_char(hextoraw('$($bucati[$j])'),'WE8MSWIN1252'); dbms_lob.writeappend(l, length(s), s);")
        }
        [void]$sb.AppendLine('  commit;')
        [void]$sb.AppendLine('end;')
        [void]$sb.AppendLine('/')
    }
    [void]$sb.AppendLine('exit')

    $r = Invoke-SqlPlus -Sql $sb.ToString() -Nume ('imp_' + $Info.Stem)
    Test-Rezultat -R $r -Context ('import ' + $Info.NumeScript)
    if ($r.Text -notmatch 'IDSCRIPT=(\d+)') { Opreste ('import ' + $Info.NumeScript + ' - nu am primit id-ul randului.') }
    [int]$Matches[1]
}

function Scrie-XmlArhiva {
    # acelasi format ca CURSORTOXML din genereazadatabasexml2: Windows-1252, CRLF, tab-uri,
    # script_appver doar cand e completat, script_content gol (continutul e in fisierul separat)
    param($Metadate, [string]$Cale)
    $sb = New-Object System.Text.StringBuilder
    [void]$sb.Append("<?xml version = `"1.0`" encoding=`"Windows-1252`" standalone=`"yes`"?>`r`n")
    [void]$sb.Append("<VFPData>`r`n")
    foreach ($m in $Metadate) {
        [void]$sb.Append("`t<crsxml>`r`n")
        [void]$sb.Append("`t`t<id>$($m.Id)</id>`r`n")
        [void]$sb.Append("`t`t<script_name>$([System.Security.SecurityElement]::Escape($m.NumeScript))</script_name>`r`n")
        [void]$sb.Append("`t`t<script_type>$([System.Security.SecurityElement]::Escape($m.Tip))</script_type>`r`n")
        [void]$sb.Append("`t`t<script_date>$($m.DataYmd)</script_date>`r`n")
        [void]$sb.Append("`t`t<script_seq>$($m.Secventa)</script_seq>`r`n")
        [void]$sb.Append("`t`t<script_order>$($m.Ordine)</script_order>`r`n")
        if ($m.AppVer) {
            [void]$sb.Append("`t`t<script_appver>$([System.Security.SecurityElement]::Escape($m.AppVer))</script_appver>`r`n")
        }
        [void]$sb.Append("`t`t<script_extra_file>$($m.Stem).sql</script_extra_file>`r`n")
        [void]$sb.Append("`t`t<script_content></script_content>`r`n")
        [void]$sb.Append("`t</crsxml>`r`n")
    }
    [void]$sb.Append("</VFPData>`r`n")
    [System.IO.File]::WriteAllText($Cale, $sb.ToString(), [System.Text.Encoding]::GetEncoding(1252))
}

function Scrie-Arhiva {
    # xml-ul primul, apoi scripturile in ordinea din xml (ca ZipOpen/ZipFile din VFP)
    param([string]$CaleZip, [string]$CaleXml, $Fisiere)
    if (Test-Path $CaleZip) { Remove-Item -LiteralPath $CaleZip -Force }
    $z = [System.IO.Compression.ZipFile]::Open($CaleZip, 'Create')
    try {
        [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($z, $CaleXml, [System.IO.Path]::GetFileName($CaleXml), 'Optimal')
        foreach ($f in $Fisiere) {
            [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($z, $f, [System.IO.Path]::GetFileName($f), 'Optimal')
        }
    } finally {
        $z.Dispose()
    }
}

function Scrie-IndexArhive {
    # roa_database.xml / roa_databasen.xml - lista arhivelor existente, sortata dupa nume de fisier
    param([string]$Prefix, [string]$Cale)
    $arhive = @(Get-ChildItem -LiteralPath $ZipPath -Filter ($Prefix + '_*.zip') -File |
                Where-Object { $_.Name -like ($Prefix + '_*.zip') } | Sort-Object Name)
    $sb = New-Object System.Text.StringBuilder
    [void]$sb.Append("<?xml version = `"1.0`" encoding=`"Windows-1252`" standalone=`"yes`"?>`r`n")
    [void]$sb.Append("<VFPData>`r`n")
    foreach ($a in $arhive) {
        $parti = [System.IO.Path]::GetFileNameWithoutExtension($a.Name) -split '_'
        if ($parti.Count -lt 3) { continue }
        [void]$sb.Append("`t<crsxml>`r`n")
        [void]$sb.Append("`t`t<url>|BASEPATH|/$($a.Name)</url>`r`n")
        [void]$sb.Append("`t`t<data1>$($parti[1])</data1>`r`n")
        [void]$sb.Append("`t`t<data2>$($parti[2])</data2>`r`n")
        [void]$sb.Append("`t</crsxml>`r`n")
    }
    [void]$sb.Append("</VFPData>`r`n")
    [System.IO.File]::WriteAllText($Cale, $sb.ToString(), [System.Text.Encoding]::GetEncoding(1252))
}

function Verifica-ArhivaPrinOracle {
    # citeste arhiva inapoi cu cititorul clientului (pack_utils.decodebase64 + zip_util_pkg.get_file)
    # si compara lungimea + MD5 al fiecarui fisier extras cu cel de pe disc
    param([string]$CaleZip, $FisiereAsteptate)
    $octeti = [System.IO.File]::ReadAllBytes($CaleZip)
    $b64 = [Convert]::ToBase64String($octeti)
    $sb = New-Object System.Text.StringBuilder
    [void]$sb.AppendLine('set serveroutput on size unlimited')
    [void]$sb.AppendLine('set pagesize 0 linesize 400 trimspool on heading off')
    [void]$sb.AppendLine('variable b_z clob')
    [void]$sb.AppendLine('begin dbms_lob.createtemporary(:b_z, true); end;')
    [void]$sb.AppendLine('/')
    # bucati multiplu de 4, ca sirul base64 sa ramana valid oriunde s-ar taia
    $pas = 1920
    for ($i = 0; $i -lt $b64.Length; $i += $pas * $BUCATI_PER_BLOC) {
        [void]$sb.AppendLine('declare s varchar2(32767);')
        [void]$sb.AppendLine('begin')
        for ($j = $i; $j -lt [Math]::Min($i + $pas * $BUCATI_PER_BLOC, $b64.Length); $j += $pas) {
            $bucata = $b64.Substring($j, [Math]::Min($pas, $b64.Length - $j))
            [void]$sb.AppendLine("  s := '$bucata'; dbms_lob.writeappend(:b_z, length(s), s);")
        }
        [void]$sb.AppendLine('end;')
        [void]$sb.AppendLine('/')
    }
    [void]$sb.AppendLine('declare')
    [void]$sb.AppendLine('  l_zip blob; l_f blob;')
    [void]$sb.AppendLine('begin')
    [void]$sb.AppendLine('  l_zip := pack_utils.decodebase64(:b_z);')
    [void]$sb.AppendLine("  dbms_output.put_line('ZIP|' || dbms_lob.getlength(l_zip));")
    foreach ($f in $FisiereAsteptate) {
        [void]$sb.AppendLine("  l_f := zip_util_pkg.get_file(l_zip, '$f');")
        [void]$sb.AppendLine("  if l_f is null then dbms_output.put_line('LIPSA|$f');")
        [void]$sb.AppendLine("  else dbms_output.put_line('FIS|$f|' || dbms_lob.getlength(l_f) || '|' || rawtohex(dbms_crypto.hash(l_f, 2))); end if;")
    }
    [void]$sb.AppendLine('end;')
    [void]$sb.AppendLine('/')
    [void]$sb.AppendLine('exit')

    $r = Invoke-SqlPlus -Sql $sb.ToString() -Nume 'verifzip'
    Test-Rezultat -R $r -Context ('verificare arhiva ' + [System.IO.Path]::GetFileName($CaleZip))
    $citite = @{}
    foreach ($linie in ($r.Text -split "`r?`n")) {
        $linie = $linie.Trim()
        if ($linie -match '^FIS\|([^|]+)\|(\d+)\|([0-9A-F]+)$') {
            $citite[$Matches[1]] = [PSCustomObject]@{ Lungime = [int]$Matches[2]; Md5 = $Matches[3] }
        } elseif ($linie -match '^LIPSA\|(.+)$') {
            Opreste ('verificare arhiva - zip_util_pkg nu gaseste ' + $Matches[1] + ' in ' + $CaleZip)
        }
    }
    $md5 = [System.Security.Cryptography.MD5]::Create()
    $erori = 0
    foreach ($f in $FisiereAsteptate) {
        if (-not $citite.ContainsKey($f)) { Write-Output ('  LIPSA   ' + $f); $erori++; continue }
        $local = Join-Path $TempDir $f
        $b = [System.IO.File]::ReadAllBytes($local)
        $h = (ConvertTo-Hex $md5.ComputeHash($b))
        if ($citite[$f].Lungime -eq $b.Length -and $citite[$f].Md5 -eq $h) {
            Write-Output ('  ok      ' + $f + ' (' + $b.Length + ' octeti)')
        } else {
            Write-Output ('  DIFERIT ' + $f + ' - disc ' + $b.Length + '/' + $h + ', extras ' + $citite[$f].Lungime + '/' + $citite[$f].Md5)
            $erori++
        }
    }
    $md5.Dispose()
    if ($erori -gt 0) { Opreste ('verificare arhiva - ' + $erori + ' fisiere nu se citesc corect prin zip_util_pkg.') }
}

# ---------- pregatire ----------
$TempDir = Join-Path $env:TEMP ('publicare_scripturi_' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $TempDir | Out-Null

if (-not (Test-Path -LiteralPath $Settings)) { Opreste ('nu gasesc settings.ini la ' + $Settings + ' - da caile cu -DirScripturi/-ZipPath/-Alias/-User/-Password sau alt -Settings.') }
$ini = Citeste-Ini $Settings

# parametrii dati explicit bat ini-ul
if (-not $PSBoundParameters.ContainsKey('User')) { $User = Ia-Cheie $ini 'connection' 'username_database' }
if (-not $PSBoundParameters.ContainsKey('Password')) { $Password = Ia-Cheie $ini 'connection' 'password_database' }
if (-not $PSBoundParameters.ContainsKey('Alias')) {
    $dsn = Ia-Cheie $ini 'connection' 'host_database'
    if ($dsn) { $Alias = Rezolva-Alias $dsn }
}
if (-not $PSBoundParameters.ContainsKey('DirScripturi')) {
    $f = Ia-Cheie $ini 'folder' 'script_folder'
    if ($f) { $DirScripturi = ($f.TrimEnd('\') -replace '\\SCRIPTURI$', '\SCRIPTURI_CLAR') }
}
if (-not $PSBoundParameters.ContainsKey('ZipPath')) {
    $f = Ia-Cheie $ini 'folder' 'roa_output'
    if ($f) { $ZipPath = (Join-Path $f '_UPDATE') }
}
if (-not $PSBoundParameters.ContainsKey('SqlPlus')) { $SqlPlus = Ia-Cheie $ini 'folder' 'sqlplus_exe' }
foreach ($p in @(@('User', $User), @('Password', $Password), @('Alias', $Alias), @('DirScripturi', $DirScripturi), @('ZipPath', $ZipPath), @('SqlPlus', $SqlPlus))) {
    if (-not $p[1]) { Opreste ('lipseste ' + $p[0] + ' - nu e in ' + $Settings + ' si nu a fost dat ca parametru.') }
}
# prefixele valide (o schema tinta per prefix), pentru validarea numelor de scripturi
$prefixe = @()
if ($ini.ContainsKey('script')) { $prefixe = @($ini['script'].Values | ForEach-Object { $_.TrimEnd('_').ToUpper() }) }

if (-not (Test-Path $SqlPlus)) { Opreste ('nu gasesc sqlplus la ' + $SqlPlus) }
$env:TNS_ADMIN = Split-Path $SqlPlus -Parent
$ConnStr = $User + '/' + $Password + '@' + $Alias

if ($DoarImport -and $DoarArhiva) { Opreste '-DoarImport si -DoarArhiva se exclud reciproc.' }

# ---------- 1. ce se publica ----------
Scrie-Titlu '1. SCRIPTURI DE PUBLICAT'
$candidati = New-Object System.Collections.ArrayList
if ($Fisier) {
    foreach ($f in $Fisier) {
        $cale = $f
        if (-not [System.IO.Path]::IsPathRooted($cale)) {
            $cale = (Resolve-Path -LiteralPath $f -ErrorAction SilentlyContinue)
            if (-not $cale) { Opreste ('nu gasesc fisierul ' + $f) }
            $cale = $cale.Path
        }
        if (-not (Test-Path -LiteralPath $cale)) { Opreste ('nu gasesc fisierul ' + $cale) }
        $info = Parseaza-NumeScript $cale
        if (-not $info) { Opreste ('nume de script neconform (<prefix>_YYYY_MM_DD_NN_<tip>.sql): ' + $cale) }
        [void]$candidati.Add($info)
    }
    $luni = @($candidati | ForEach-Object { $_.Data.ToString('yyyy-MM') } | Sort-Object -Unique)
    if ($luni.Count -gt 1) { Opreste ('scripturile date sunt din luni diferite (' + ($luni -join ', ') + ') - ruleaza cate o luna.') }
    if (-not $Luna) { $Luna = $luni[0] }
} else {
    if (-not $Luna) { $Luna = (Get-Date).ToString('yyyy-MM') }
}

if ($Luna -notmatch '^(\d{4})-(\d{2})$') { Opreste ('-Luna trebuie in formatul YYYY-MM (primit: ' + $Luna + ')') }
$anLuna = [int]$Matches[1]
$lunaLuna = [int]$Matches[2]
$dataInceput = Get-Date -Year $anLuna -Month $lunaLuna -Day 1 -Hour 0 -Minute 0 -Second 0
$dataSfarsit = $dataInceput.AddMonths(1).AddDays(-1)
$etichetaLuna = $dataInceput.ToString('yyyyMMdd') + '_' + $dataSfarsit.ToString('yyyyMMdd')

$dirLuna = Join-Path (Join-Path $DirScripturi $dataInceput.ToString('yyyy')) $dataInceput.ToString('MM')
if (-not $Fisier -and -not $DoarArhiva) {
    if (-not (Test-Path -LiteralPath $dirLuna)) { Opreste ('nu gasesc directorul lunii: ' + $dirLuna) }
    foreach ($f in (Get-ChildItem -LiteralPath $dirLuna -Filter '*.sql' -File | Sort-Object Name)) {
        $info = Parseaza-NumeScript $f.FullName
        if (-not $info) { Write-Output ('  [sarit] nume neconform: ' + $f.Name); continue }
        [void]$candidati.Add($info)
    }
}

Write-Output ('Luna: ' + $Luna + '   arhiva: ' + $etichetaLuna)
Write-Output ('Configurare: ' + $Settings)
Write-Output ('Baza: ' + $User + '@' + $Alias + ' (' + $SqlPlus + ')')
Write-Output ('Scripturi: ' + $dirLuna)
Write-Output ('Arhive: ' + $ZipPath)

foreach ($c in $candidati) {
    if ($prefixe.Count -gt 0 -and $prefixe -notcontains $c.Prefix) {
        Write-Output ('  [atentie] prefix necunoscut "' + $c.Prefix + '" in ' + [System.IO.Path]::GetFileName($c.Cale) + ' (in ini: ' + ($prefixe -join ', ') + ')')
    }
    $octeti = [System.IO.File]::ReadAllBytes($c.Cale)
    if ($octeti.Length -eq 0) { Opreste ('fisier gol: ' + $c.Cale) }
    $text = [System.Text.Encoding]::GetEncoding(1252).GetString($octeti)
    if ($text -match "(?<!`r)`n") { Opreste ('sfarsituri de linie LF in ' + $c.Cale + ' - scripturile trebuie salvate cu CRLF (parsarea pe client se face pe CHR(13)+CHR(10)).') }
    Add-Member -InputObject $c -NotePropertyName Octeti -NotePropertyValue $octeti
}

# ---------- 2. starea din baza ----------
Scrie-Titlu '2. STAREA DIN UPD_DATABASE'
$metadate = Citeste-MetadateLuna -Inceput $dataInceput -Sfarsit $dataSfarsit
Write-Output ('In baza, pentru luna ' + $Luna + ': ' + $metadate.Count + ' scripturi.')
$dupaNume = @{}
foreach ($m in $metadate) { $dupaNume[$m.NumeScript] = $m }

$deImportat = New-Object System.Collections.ArrayList
if (-not $DoarArhiva) {
    $existente = @($candidati | Where-Object { $dupaNume.ContainsKey($_.NumeScript) })
    $continuturi = @{}
    if ($existente.Count -gt 0) {
        $continuturi = Citeste-Continut -Metadate @($existente | ForEach-Object { $dupaNume[$_.NumeScript] })
    }
    foreach ($c in $candidati) {
        if (-not $dupaNume.ContainsKey($c.NumeScript)) {
            Write-Output ('  NOU        ' + $c.NumeScript + ' (' + $c.Octeti.Length + ' octeti)')
            [void]$deImportat.Add($c)
            continue
        }
        $m = $dupaNume[$c.NumeScript]
        $inBaza = $continuturi[$m.Id]
        $identic = Compara-Octeti -A $inBaza -B $c.Octeti
        if ($identic -and -not $Toate) {
            Write-Output ('  publicat   ' + $c.NumeScript + ' (id ' + $m.Id + ', identic)')
        } elseif ($identic) {
            Write-Output ('  REPUBLIC   ' + $c.NumeScript + ' (id ' + $m.Id + ', identic, fortat cu -Toate)')
            [void]$deImportat.Add($c)
        } else {
            Write-Output ('  MODIFICAT  ' + $c.NumeScript + ' (id ' + $m.Id + ', in baza ' + $(if ($inBaza) { $inBaza.Length } else { 0 }) + ' octeti, pe disc ' + $c.Octeti.Length + ')')
            [void]$deImportat.Add($c)
        }
    }
}

# ---------- 3. import ----------
if (-not $DoarArhiva) {
    Scrie-Titlu '3. IMPORT IN UPD_DATABASE'
    if ($deImportat.Count -eq 0) {
        Write-Output 'Nimic de importat - toate scripturile din luna sunt deja publicate cu acelasi continut.'
    } elseif ($DryRun) {
        Write-Output ('DryRun - as importa ' + $deImportat.Count + ' scripturi (vezi lista de mai sus).')
    } else {
        foreach ($c in $deImportat) {
            $id = Importa-Script -Info $c -Octeti $c.Octeti
            Write-Output ('  importat   ' + $c.NumeScript + ' -> id ' + $id)
        }
        $metadate = Citeste-MetadateLuna -Inceput $dataInceput -Sfarsit $dataSfarsit
        $dupaNume = @{}
        foreach ($m in $metadate) { $dupaNume[$m.NumeScript] = $m }
        $verif = Citeste-Continut -Metadate @($deImportat | ForEach-Object { $dupaNume[$_.NumeScript] })
        $erori = 0
        foreach ($c in $deImportat) {
            $m = $dupaNume[$c.NumeScript]
            $inBaza = $verif[$m.Id]
            if (Compara-Octeti -A $inBaza -B $c.Octeti) {
                Write-Output ('  verificat  ' + $c.NumeScript + ' (' + $c.Octeti.Length + ' octeti, identic cu fisierul)')
            } else {
                Write-Output ('  DIFERIT    ' + $c.NumeScript + ' - continutul din baza nu coincide cu fisierul!')
                $erori++
            }
        }
        if ($erori -gt 0) { Opreste ($erori + ' scripturi nu s-au scris corect in baza - nu generez arhiva.') }
    }
}

if ($DoarImport) {
    Scrie-Titlu 'GATA (doar import)'
    Write-Output 'Arhiva NU a fost regenerata - clientii primesc scripturile abia dupa pasul de arhiva.'
    Curata-Temp
    exit 0
}

# ---------- 4. arhiva lunara ----------
Scrie-Titlu '4. ARHIVA LUNARA'
if (-not (Test-Path -LiteralPath $ZipPath)) { Opreste ('nu gasesc directorul arhivelor: ' + $ZipPath) }
if ($DryRun) {
    Write-Output ('DryRun - as regenera database_' + $etichetaLuna + '.zip si databasen_' + $etichetaLuna + '.zip din ' + $metadate.Count + ' scripturi.')
    Curata-Temp
    exit 0
}

$metadate = Citeste-MetadateLuna -Inceput $dataInceput -Sfarsit $dataSfarsit
if ($metadate.Count -eq 0) { Opreste ('nu exista scripturi in UPD_DATABASE pentru luna ' + $Luna + ' - nu am ce arhiva.') }

$continuturi = Citeste-Continut -Metadate $metadate
$fisiereSql = New-Object System.Collections.ArrayList
foreach ($m in $metadate) {
    $b = $continuturi[$m.Id]
    if (-not $b -or $b.Length -ne $m.Lungime) {
        Opreste ('citire incompleta pentru ' + $m.NumeScript + ' (asteptat ' + $m.Lungime + ' octeti, citit ' + $(if ($b) { $b.Length } else { 0 }) + ').')
    }
    $numeFis = $m.Stem + '.sql'
    [System.IO.File]::WriteAllBytes((Join-Path $TempDir $numeFis), $b)
    [void]$fisiereSql.Add($numeFis)
}
Write-Output ('Extrase din baza: ' + $fisiereSql.Count + ' scripturi.')

$numeFisiere = @($fisiereSql)
$rezultat = @()
foreach ($prefix in @('database', 'databasen')) {
    $numeXml = $prefix + '_' + $etichetaLuna + '.xml'
    $caleXml = Join-Path $TempDir $numeXml
    Scrie-XmlArhiva -Metadate $metadate -Cale $caleXml
    $caleZip = Join-Path $TempDir ($prefix + '_' + $etichetaLuna + '.zip')
    Scrie-Arhiva -CaleZip $caleZip -CaleXml $caleXml -Fisiere @($numeFisiere | ForEach-Object { Join-Path $TempDir $_ })
    Write-Output ('Generat ' + [System.IO.Path]::GetFileName($caleZip) + ' (' + (Get-Item -LiteralPath $caleZip).Length + ' octeti)')
    $rezultat += [PSCustomObject]@{ Zip = $caleZip; Xml = $numeXml }
}

# ---------- 5. verificare cu cititorul clientului ----------
if (-not $FaraVerificareZip) {
    Scrie-Titlu '5. VERIFICARE ARHIVA PRIN ZIP_UTIL_PKG'
    Verifica-ArhivaPrinOracle -CaleZip $rezultat[0].Zip -FisiereAsteptate (@($rezultat[0].Xml) + $numeFisiere)
} else {
    Scrie-Titlu '5. VERIFICARE ARHIVA PRIN ZIP_UTIL_PKG'
    Write-Output 'Sarit (-FaraVerificareZip).'
}

# ---------- 6. publicare in _UPDATE ----------
Scrie-Titlu '6. PUBLICARE IN _UPDATE'
if (-not $BackupPath) { $BackupPath = Join-Path $ZipPath ('_backup\' + (Get-Date).ToString('yyyyMMdd_HHmmss')) }
New-Item -ItemType Directory -Force -Path $BackupPath | Out-Null
foreach ($n in @(($rezultat | ForEach-Object { [System.IO.Path]::GetFileName($_.Zip) }) + 'roa_database.xml' + 'roa_databasen.xml')) {
    $tinta = Join-Path $ZipPath $n
    if (Test-Path -LiteralPath $tinta) {
        Copy-Item -LiteralPath $tinta -Destination (Join-Path $BackupPath $n) -Force
        Write-Output ('  salvat  ' + $n + ' -> ' + $BackupPath)
    }
}
foreach ($r in $rezultat) {
    Copy-Item -LiteralPath $r.Zip -Destination (Join-Path $ZipPath ([System.IO.Path]::GetFileName($r.Zip))) -Force
    Write-Output ('  publicat ' + [System.IO.Path]::GetFileName($r.Zip))
}
Scrie-IndexArhive -Prefix 'database' -Cale (Join-Path $ZipPath 'roa_database.xml')
Scrie-IndexArhive -Prefix 'databasen' -Cale (Join-Path $ZipPath 'roa_databasen.xml')
Write-Output '  publicat roa_database.xml, roa_databasen.xml'

Scrie-Titlu 'GATA'
Write-Output ('Luna ' + $Luna + ': ' + $metadate.Count + ' scripturi in arhiva, ' + $deImportat.Count + ' importate acum.')
Write-Output ('Backup-ul fisierelor inlocuite: ' + $BackupPath)
Curata-Temp
exit 0
