param(
    [string]$Server = $env:ONEC_SQL_SERVER,
    [string]$Database = $env:ONEC_SQL_DATABASE,
    [string]$User = $env:ONEC_SQL_USER,
    [string]$Password = $env:ONEC_SQL_PASSWORD,
    [string]$BaseMetadataDir = "reports\1c-sql\upo\structured-metadata-all-kinds",
    [string]$ExtensionGuidIndex = "reports\1c-sql\upo\xml-guid-index-extensions.json",
    [string]$Output
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

if (-not $Server) { throw "Server is required. Use -Server or ONEC_SQL_SERVER." }
if (-not $Database) { throw "Database is required. Use -Database or ONEC_SQL_DATABASE." }
if (-not $User) { throw "User is required. Use -User or ONEC_SQL_USER." }
if (-not $Password) { throw "Password is required. Use -Password or ONEC_SQL_PASSWORD." }

function U {
    param([string]$Base64)
    return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Base64))
}

$ruExtension = U "0KDQsNGB0YjQuNGA0LXQvdC40LU="
$ruConfigObject = U "0J7QsdGK0LXQutGC0JrQvtC90YTQuNCz0YPRgNCw0YbQuNC4"
$ruForm = U "0KTQvtGA0LzQsA=="

$kindRu = @{
    Configuration = (U "0JrQvtC90YTQuNCz0YPRgNCw0YbQuNGP")
    CommonModule = (U "0J7QsdGJ0LjQudCc0L7QtNGD0LvRjA==")
    CommonForm = (U "0J7QsdGJ0LDRj9Ck0L7RgNC80LA=")
    CommonCommand = (U "0J7QsdGJ0LDRj9Ca0L7QvNCw0L3QtNCw")
    CommonAttribute = (U "0J7QsdGJ0LjQudCg0LXQutCy0LjQt9C40YI=")
    CommonTemplate = (U "0J7QsdGJ0LjQudCc0LDQutC10YI=")
    CommonPicture = (U "0J7QsdGJ0LDRj9Ca0LDRgNGC0LjQvdC60LA=")
    Catalog = (U "0KHQv9GA0LDQstC+0YfQvdC40Lo=")
    Document = (U "0JTQvtC60YPQvNC10L3Rgg==")
    DataProcessor = (U "0J7QsdGA0LDQsdC+0YLQutCw")
    Report = (U "0J7RgtGH0LXRgg==")
    InformationRegister = (U "0KDQtdCz0LjRgdGC0YDQodCy0LXQtNC10L3QuNC5")
    AccumulationRegister = (U "0KDQtdCz0LjRgdGC0YDQndCw0LrQvtC/0LvQtdC90LjRjw==")
    AccountingRegister = (U "0KDQtdCz0LjRgdGC0YDQkdGD0YXQs9Cw0LvRgtC10YDQuNC4")
    CalculationRegister = (U "0KDQtdCz0LjRgdGC0YDQoNCw0YHRh9C10YLQsA==")
    Enum = (U "0J/QtdGA0LXRh9C40YHQu9C10L3QuNC1")
    Form = $ruForm
    Template = (U "0JzQsNC60LXRgg==")
    Role = (U "0KDQvtC70Yw=")
    Subsystem = (U "0J/QvtC00YHQuNGB0YLQtdC80LA=")
    ExchangePlan = (U "0J/Qu9Cw0L3QntCx0LzQtdC90LA=")
    BusinessProcess = (U "0JHQuNC30L3QtdGB0J/RgNC+0YbQtdGB0YE=")
    Task = (U "0JfQsNC00LDRh9Cw")
    Constant = (U "0JrQvtC90YHRgtCw0L3RgtCw")
    ChartOfCharacteristicTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KXQsNGA0LDQutGC0LXRgNC40YHRgtC40Lo=")
    ChartOfAccounts = (U "0J/Qu9Cw0L3QodGH0LXRgtC+0LI=")
    ChartOfCalculationTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KDQsNGB0YfQtdGC0LA=")
}

$folderKind = @{
    CommonModules = "CommonModule"
    CommonForms = "CommonForm"
    CommonCommands = "CommonCommand"
    CommonAttributes = "CommonAttribute"
    CommonTemplates = "CommonTemplate"
    CommonPictures = "CommonPicture"
    Catalogs = "Catalog"
    Documents = "Document"
    DataProcessors = "DataProcessor"
    Reports = "Report"
    InformationRegisters = "InformationRegister"
    AccumulationRegisters = "AccumulationRegister"
    AccountingRegisters = "AccountingRegister"
    CalculationRegisters = "CalculationRegister"
    Enums = "Enum"
    Forms = "Form"
    Templates = "Template"
    Roles = "Role"
    Subsystems = "Subsystem"
    ExchangePlans = "ExchangePlan"
    BusinessProcesses = "BusinessProcess"
    Tasks = "Task"
    Constants = "Constant"
    ChartsOfCharacteristicTypes = "ChartOfCharacteristicTypes"
    ChartsOfAccounts = "ChartOfAccounts"
    ChartsOfCalculationTypes = "ChartOfCalculationTypes"
}

function Convert-RefBytesToGuid {
    param([byte[]]$Bytes)
    if ($Bytes.Length -ne 16) { return $null }
    $hex = ($Bytes | ForEach-Object { $_.ToString("x2") }) -join ""
    return "{0}-{1}-{2}-{3}-{4}" -f $hex.Substring(24, 8), $hex.Substring(20, 4), $hex.Substring(16, 4), $hex.Substring(0, 4), $hex.Substring(4, 12)
}

function Convert-ToSha256Hex {
    param([byte[]]$Bytes)
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        return (($sha.ComputeHash($Bytes) | ForEach-Object { $_.ToString("x2") }) -join "")
    }
    finally {
        $sha.Dispose()
    }
}

function Join-ByteArrays {
    param([object[]]$Rows)
    $ordered = @($Rows | Sort-Object part_no)
    $total = 0
    foreach ($row in $ordered) { $total += $row.bytes.Length }
    $buffer = [byte[]]::new($total)
    $offset = 0
    foreach ($row in $ordered) {
        [Array]::Copy($row.bytes, 0, $buffer, $offset, $row.bytes.Length)
        $offset += $row.bytes.Length
    }
    return $buffer
}

function New-Connection {
    $connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Application Name=Codex 1C Saved State Object Compare;"
    $connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
    $connection.Open()
    return $connection
}

function Read-TableFiles {
    param(
        [System.Data.SqlClient.SqlConnection]$Connection,
        [string]$Table,
        [string[]]$FileNames
    )
    $command = $Connection.CreateCommand()
    if ($FileNames -and $FileNames.Count -gt 0) {
        $placeholders = @()
        for ($i = 0; $i -lt $FileNames.Count; $i++) {
            $paramName = "@p$i"
            $placeholders += $paramName
            $null = $command.Parameters.Add($paramName, [System.Data.SqlDbType]::NVarChar, 512)
            $command.Parameters[$paramName].Value = $FileNames[$i]
        }
        $command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] WHERE FileName IN ($($placeholders -join ', ')) ORDER BY FileName, PartNo"
    }
    else {
        $command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] ORDER BY FileName, PartNo"
    }
    $reader = $null
    $groups = @{}
    try {
        $reader = $command.ExecuteReader()
        while ($reader.Read()) {
            $fileName = [string]$reader.GetValue(0)
            if (-not $groups.ContainsKey($fileName)) { $groups[$fileName] = @() }
            $groups[$fileName] += [pscustomobject]@{
                part_no = [int]$reader.GetValue(1)
                bytes = [byte[]]$reader.GetValue(2)
            }
        }
    }
    finally {
        if ($reader -ne $null) { $reader.Dispose() }
        if ($command -ne $null) { $command.Dispose() }
    }

    $result = @{}
    foreach ($fileName in $groups.Keys) {
        $bytes = Join-ByteArrays @($groups[$fileName])
        $result[$fileName] = [pscustomobject]@{
            file_name = $fileName
            chunks = @($groups[$fileName]).Count
            bytes = $bytes.Length
            sha256 = Convert-ToSha256Hex $bytes
        }
    }
    return $result
}

function Read-Extensions {
    param([System.Data.SqlClient.SqlConnection]$Connection)
    $command = $Connection.CreateCommand()
    $command.CommandText = "SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionUsePurpose], [_ExtensionScope] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName]"
    $reader = $null
    $map = @{}
    try {
        $reader = $command.ExecuteReader()
        while ($reader.Read()) {
            $bytes = [byte[]]$reader.GetValue(0)
            $guid = Convert-RefBytesToGuid $bytes
            $map[$guid] = [pscustomobject]@{
                guid = $guid
                name = [string]$reader.GetValue(1)
                order = [decimal]$reader.GetValue(2)
                use_purpose = [decimal]$reader.GetValue(3)
                scope = [decimal]$reader.GetValue(4)
            }
        }
    }
    finally {
        if ($reader -ne $null) { $reader.Dispose() }
        if ($command -ne $null) { $command.Dispose() }
    }
    return $map
}

function Get-BaseObjectInfo {
    param([string]$Guid)
    $metadataRoot = [System.IO.Path]::GetFullPath($BaseMetadataDir)
    if (-not (Test-Path -LiteralPath $metadataRoot)) { return $null }
    $match = Get-ChildItem -LiteralPath $metadataRoot -Recurse -Filter *.json -File |
        Select-String -SimpleMatch $Guid -List |
        Select-Object -First 1
    if (-not $match) { return $null }
    $data = Get-Content -LiteralPath $match.Path -Raw -Encoding UTF8 | ConvertFrom-Json
    $kind = [string]$data.kind
    $name = [string]$data.identity.name
    $synonym = $null
    if ($data.identity.synonyms -and $data.identity.synonyms.ru) { $synonym = [string]$data.identity.synonyms.ru }
    return [pscustomobject]@{
        layer = "base"
        kind = $kind
        kind_ru = if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }
        name = $name
        synonym = $synonym
        full_name = "$(if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }).$name"
        guid = $Guid
        evidence = [pscustomobject]@{
            metadata_file = $match.Path
            xml_file = $data.xml_file
        }
    }
}

function Get-ExtensionGuidMap {
    $path = [System.IO.Path]::GetFullPath($ExtensionGuidIndex)
    if (-not (Test-Path -LiteralPath $path)) { return @{} }
    $data = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
    return $data.guid_map
}

function Get-ExtensionObjectInfo {
    param(
        [string]$ExtensionName,
        [string]$ObjectGuid,
        [object]$GuidMap
    )
    $entry = $GuidMap.$ObjectGuid
    $top = $null
    if ($entry -and $entry.top_objects -and @($entry.top_objects).Count -gt 0) {
        $top = @($entry.top_objects)[0]
    }
    $relativePath = if ($top) { [string]$top.relative_path } else { $null }
    $parts = if ($relativePath) { $relativePath -split "[\\/]" } else { @() }
    $parentKind = $null
    $parentName = $null
    $artifactKind = if ($top) { [string]$top.xml_kind } else { "ConfigCASObject" }
    $artifactName = if ($top) { [string]$top.name } else { $ObjectGuid }
    $synonym = if ($top -and $top.synonym) { [string]$top.synonym } else { $null }
    for ($i = 0; $i -lt $parts.Count - 1; $i++) {
        if ($folderKind.ContainsKey($parts[$i]) -and $parts[$i] -ne "Forms" -and $parts[$i] -ne "Templates") {
            $parentKind = $folderKind[$parts[$i]]
            if ($i + 1 -lt $parts.Count) { $parentName = $parts[$i + 1] }
            break
        }
    }
    $artifactKindRu = if ($kindRu.ContainsKey($artifactKind)) { $kindRu[$artifactKind] } else { $artifactKind }
    $parentKindRu = if ($parentKind -and $kindRu.ContainsKey($parentKind)) { $kindRu[$parentKind] } else { $parentKind }
    $fullName = if ($parentKind -and $artifactKind -eq "Form") {
        "$ruExtension.$ExtensionName.$parentKindRu.$parentName.$ruForm.$artifactName"
    }
    elseif ($parentKind) {
        "$ruExtension.$ExtensionName.$parentKindRu.$parentName.$artifactKindRu.$artifactName"
    }
    else {
        "$ruExtension.$ExtensionName.$artifactKindRu.$artifactName"
    }
    return [pscustomobject]@{
        layer = "extension"
        extension = $ExtensionName
        parent_kind = $parentKind
        parent_kind_ru = $parentKindRu
        parent_name = $parentName
        kind = $artifactKind
        kind_ru = $artifactKindRu
        name = $artifactName
        synonym = $synonym
        full_name = $fullName
        guid = $ObjectGuid
        relative_path = $relativePath
        evidence = [pscustomobject]@{
            xml_path = if ($top) { [string]$top.path } else { $null }
        }
    }
}

function Split-ObjectPart {
    param([string]$FileName)
    if ($FileName -match '^([0-9a-fA-F-]{36})(\..+)?$') {
        return [pscustomobject]@{ object_id = $Matches[1].ToLowerInvariant(); suffix = if ($Matches[2]) { $Matches[2] } else { "" } }
    }
    return $null
}

function Split-ExtensionFileName {
    param([string]$FileName)
    if ($FileName -match '^([0-9a-fA-F-]{36})__(.+)$') {
        $rest = $Matches[2]
        if ($rest -eq "configinfo") {
            return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $null; suffix = ""; system_name = "configinfo" }
        }
        $part = Split-ObjectPart $rest
        return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $part.object_id; suffix = $part.suffix; system_name = $null }
    }
    return $null
}

function New-StorageEvidence {
    param(
        [string]$SavedTable,
        [string]$ActiveTable,
        [string]$FileName,
        [object]$Saved,
        [object]$Active
    )
    return [pscustomobject]@{
        saved_table = $SavedTable
        active_table = $ActiveTable
        file_name = $FileName
        saved_bytes = $Saved.bytes
        active_bytes = if ($Active) { $Active.bytes } else { $null }
        saved_sha256 = $Saved.sha256
        active_sha256 = if ($Active) { $Active.sha256 } else { $null }
        active_exists = [bool]$Active
        changed = (-not $Active) -or ($Saved.sha256 -ne $Active.sha256) -or ($Saved.bytes -ne $Active.bytes)
    }
}

$connection = New-Connection
try {
    $configSave = Read-TableFiles $connection "ConfigSave" $null
    $configCassave = Read-TableFiles $connection "ConfigCASSave" $null
    $configActive = Read-TableFiles $connection "Config" @($configSave.Keys)
    $configCasActive = Read-TableFiles $connection "ConfigCAS" @($configCassave.Keys)
    $extensions = Read-Extensions $connection
}
finally {
    if ($connection -ne $null) { $connection.Dispose() }
}

$objectChangesByKey = @{}
$systemChanges = @()

foreach ($fileName in $configSave.Keys) {
    $saved = $configSave[$fileName]
    $active = if ($configActive.ContainsKey($fileName)) { $configActive[$fileName] } else { $null }
    $evidence = New-StorageEvidence "ConfigSave" "Config" $fileName $saved $active
    if (-not $evidence.changed) { continue }
    $part = Split-ObjectPart $fileName
    if (-not $part) {
        $systemChanges += [pscustomobject]@{ layer = "base"; name = $fileName; storage = $evidence }
        continue
    }
    $info = Get-BaseObjectInfo $part.object_id
    if (-not $info) {
        $info = [pscustomobject]@{
            layer = "base"
            kind = "ConfigObject"
            kind_ru = $ruConfigObject
            name = $part.object_id
            synonym = $null
            full_name = "$ruConfigObject.$($part.object_id)"
            guid = $part.object_id
            evidence = $null
        }
    }
    $key = "base::$($part.object_id)"
    if (-not $objectChangesByKey.ContainsKey($key)) {
        $objectChangesByKey[$key] = [ordered]@{
            layer = $info.layer
            kind = $info.kind
            kind_ru = $info.kind_ru
            name = $info.name
            synonym = $info.synonym
            full_name = $info.full_name
            guid = $info.guid
            change_state = "saved_not_applied"
            storage = @()
            evidence = $info.evidence
        }
    }
    $objectChangesByKey[$key].storage += $evidence
}

$extensionGuidMap = Get-ExtensionGuidMap
foreach ($fileName in $configCassave.Keys) {
    $saved = $configCassave[$fileName]
    $active = if ($configCasActive.ContainsKey($fileName)) { $configCasActive[$fileName] } else { $null }
    $evidence = New-StorageEvidence "ConfigCASSave" "ConfigCAS" $fileName $saved $active
    if (-not $evidence.changed) { continue }
    $part = Split-ExtensionFileName $fileName
    if (-not $part) {
        $systemChanges += [pscustomobject]@{ layer = "extension"; name = $fileName; storage = $evidence }
        continue
    }
    $extension = if ($extensions.ContainsKey($part.extension_id)) { $extensions[$part.extension_id] } else { $null }
    $extensionName = if ($extension) { $extension.name } else { $part.extension_id }
    if ($part.system_name) {
        $systemChanges += [pscustomobject]@{ layer = "extension"; extension = $extensionName; name = $part.system_name; storage = $evidence }
        continue
    }
    $info = Get-ExtensionObjectInfo $extensionName $part.object_id $extensionGuidMap
    $key = "extension::$($part.extension_id)::$($part.object_id)"
    if (-not $objectChangesByKey.ContainsKey($key)) {
        $objectChangesByKey[$key] = [ordered]@{
            layer = $info.layer
            extension = $info.extension
            parent_kind = $info.parent_kind
            parent_kind_ru = $info.parent_kind_ru
            parent_name = $info.parent_name
            kind = $info.kind
            kind_ru = $info.kind_ru
            name = $info.name
            synonym = $info.synonym
            full_name = $info.full_name
            guid = $info.guid
            relative_path = $info.relative_path
            change_state = "saved_not_applied"
            storage = @()
            evidence = $info.evidence
        }
    }
    $objectChangesByKey[$key].storage += $evidence
}

$objectChanges = @($objectChangesByKey.Values | ForEach-Object { [pscustomobject]$_ } | Sort-Object layer, extension, full_name)
$result = [pscustomobject]@{
    schema = "onec_saved_state_object_comparison.v1"
    server = $Server
    database = $Database
    view = "saved_not_applied_vs_active"
    object_changes = $objectChanges
    system_changes = $systemChanges
    counts = [pscustomobject]@{
        object_changes = $objectChanges.Count
        system_changes = $systemChanges.Count
        config_save_files = $configSave.Count
        config_cas_save_files = $configCassave.Count
    }
    safety = [pscustomobject]@{
        read_only = $true
        sql_write_performed = $false
        exposes_storage_evidence = $true
        public_terms_are_1c_objects = $true
    }
}

$json = $result | ConvertTo-Json -Depth 20
if ($Output) {
    $parent = Split-Path -Parent $Output
    if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
    $json | Set-Content -LiteralPath $Output -Encoding UTF8
}
$json
