# role-info v1.6 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Mandatory=$true, Position=0)][Alias('Path')][string]$RightsPath,
	[switch]$ShowDenied,
	[int]$Limit = 150,
	[int]$Offset = 0,
	[string]$OutFile
)

$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# --- Output helper (always collect, paginate at the end) ---
$script:lines = @()
function Out([string]$text) { $script:lines += $text }

# --- Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($RightsPath)) {
	$RightsPath = Join-Path (Get-Location).Path $RightsPath
}

if (-not (Test-Path $RightsPath)) {
	Write-Host "[ERROR] File not found: $RightsPath"
	exit 1
}

# --- Try to find metadata file for role name/synonym ---
$roleName = ""
$roleSynonym = ""
$extDir = Split-Path $RightsPath          # .../Ext
$roleDir = Split-Path $extDir             # .../RoleName
$rolesDir = Split-Path $roleDir           # .../Roles
$roleFolderName = Split-Path $roleDir -Leaf
$metaPath = Join-Path $rolesDir "$roleFolderName.xml"

if (Test-Path $metaPath) {
	try {
		[xml]$metaXml = Get-Content -Path $metaPath -Encoding UTF8
		$ns = New-Object System.Xml.XmlNamespaceManager($metaXml.NameTable)
		$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
		$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
		$nameNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Name", $ns)
		if ($nameNode) { $roleName = $nameNode.InnerText }
		$synNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
		if ($synNode) { $roleSynonym = $synNode.InnerText }
	} catch {
		# Ignore metadata parsing errors
	}
}

if (-not $roleName) { $roleName = $roleFolderName }

# --- Parse Rights.xml ---
[xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8
$root = $xml.DocumentElement
$rightsNs = "http://v8.1c.ru/8.2/roles"

# Global flags
$setForNew = $root.setForNewObjects
$setForAttrs = $root.setForAttributesByDefault
$independentChild = $root.independentRightsOfChildObjects

# --- Collect objects ---
# Structure: grouped by type prefix, then by object short name
$allowed = [ordered]@{}     # type -> [ordered]@{ shortName -> [list of rights] }
$denied = [ordered]@{}      # type -> [ordered]@{ shortName -> [list of rights] }
$rlsObjects = @()
$totalAllowed = 0
$totalDenied = 0

$objects = $root.GetElementsByTagName("object", $rightsNs)
foreach ($obj in $objects) {
	$objName = ""
	$rights = @()

	foreach ($child in $obj.ChildNodes) {
		if ($child.LocalName -eq "name" -and $child.NamespaceURI -eq $rightsNs) {
			$objName = $child.InnerText
		}
		if ($child.LocalName -eq "right" -and $child.NamespaceURI -eq $rightsNs) {
			$rName = ""
			$rValue = ""
			$hasRLS = $false
			foreach ($rc in $child.ChildNodes) {
				if ($rc.LocalName -eq "name") { $rName = $rc.InnerText }
				if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText }
				if ($rc.LocalName -eq "restrictionByCondition") { $hasRLS = $true }
			}
			if ($rName -and $rValue) {
				$rights += @{ name = $rName; value = $rValue; rls = $hasRLS }
			}
		}
	}

	if (-not $objName -or $rights.Count -eq 0) { continue }

	# Split into type prefix and short name
	$dotIdx = $objName.IndexOf(".")
	if ($dotIdx -lt 0) { continue }
	$typePrefix = $objName.Substring(0, $dotIdx)
	$shortName = $objName.Substring($dotIdx + 1)

	foreach ($r in $rights) {
		if ($r.value -eq "true") {
			$totalAllowed++
			if (-not $allowed.Contains($typePrefix)) {
				$allowed[$typePrefix] = [ordered]@{}
			}
			if (-not $allowed[$typePrefix].Contains($shortName)) {
				$allowed[$typePrefix][$shortName] = @()
			}
			$suffix = $r.name
			if ($r.rls) {
				$suffix += " [RLS]"
				$rlsObjects += "$typePrefix.$shortName ($($r.name))"
			}
			$allowed[$typePrefix][$shortName] += $suffix
		}
		else {
			$totalDenied++
			if (-not $denied.Contains($typePrefix)) {
				$denied[$typePrefix] = [ordered]@{}
			}
			if (-not $denied[$typePrefix].Contains($shortName)) {
				$denied[$typePrefix][$shortName] = @()
			}
			$denied[$typePrefix][$shortName] += $r.name
		}
	}
}

# --- Restriction templates ---
$templates = @()
$tplNodes = $root.GetElementsByTagName("restrictionTemplate", $rightsNs)
foreach ($tpl in $tplNodes) {
	foreach ($child in $tpl.ChildNodes) {
		if ($child.LocalName -eq "name") {
			$tName = $child.InnerText
			# Extract just the name part before parentheses
			$parenIdx = $tName.IndexOf("(")
			if ($parenIdx -gt 0) { $tName = $tName.Substring(0, $parenIdx) }
			$templates += $tName
		}
	}
}

function Test-ExternalObjectRoot([string]$xmlPath) {
	if (-not (Test-Path $xmlPath)) { return $false }
	try {
		[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
		$el = $mx.DocumentElement.FirstChild
		while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
		if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
	} catch {}
	return $false
}
function Get-SupportStatusForPath([string]$targetPath) {
	try {
		$rp = (Resolve-Path $targetPath).Path
		$elemUuid = $null
		$binPath = $null
		# Reads the uuid of the first metadata element in an .xml file (or $null).
		function Get-RootUuid([string]$xmlPath) {
			if (-not (Test-Path $xmlPath)) { return $null }
			try {
				[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
				$el = $mx.DocumentElement.FirstChild
				while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
				if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
			} catch {}
			return $null
		}
		# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
		$elemUuid = Get-RootUuid $rp
		if (Test-ExternalObjectRoot $rp) { return $null }
		$d = [System.IO.Path]::GetDirectoryName($rp)
		for ($i = 0; $i -lt 12 -and $d; $i++) {
			if (Test-ExternalObjectRoot "$d.xml") { return $null }
			if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
			if (-not $binPath) {
				$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
				if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
			}
			if ($elemUuid -and $binPath) { break }
			$parent = [System.IO.Path]::GetDirectoryName($d)
			if ($parent -eq $d) { break }
			$d = $parent
		}
		if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
		$bytes = [System.IO.File]::ReadAllBytes($binPath)
		if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
		$start = 0
		if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
		$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
		$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
		if (-not $h.Success) { return "не на поддержке" }
		$G = [int]$h.Groups[1].Value
		$K = [int]$h.Groups[2].Value
		if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
		if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
		if (-not $elemUuid) { return "не на поддержке" }
		$u = [regex]::Escape($elemUuid.ToLower())
		$best = $null
		foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
			$f1 = [int]$m.Groups[1].Value
			if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
		}
		if ($null -eq $best) { return "не на поддержке" }
		switch ($best) {
			0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
			1 { return "редактируется с сохранением поддержки" }
			2 { return "снято с поддержки (правки свободны)" }
		}
		return "не на поддержке"
	} catch { return "не на поддержке" }
}

# --- Output ---
$header = "=== Role: $roleName"
if ($roleSynonym) { $header += " --- `"$roleSynonym`"" }
$header += " ==="
Out $header
$support = Get-SupportStatusForPath $RightsPath
if ($null -ne $support) { Out "Поддержка: $support" }
Out ""

Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild"
Out ""

# Helper: output group
function OutGroup($objMap, [string]$prefix, [switch]$isDenied) {
	foreach ($shortName in @($objMap.Keys)) {
		if ($isDenied) {
			$rightsList = ($objMap[$shortName] | ForEach-Object { "-$_" }) -join ", "
		} else {
			$rightsList = $objMap[$shortName] -join ", "
		}
		Out "    ${shortName}: $rightsList"
	}
}

# Allowed rights grouped by type
if ($allowed.Count -gt 0) {
	Out "Allowed rights:"
	Out ""
	foreach ($typePrefix in $allowed.Keys) {
		$objMap = $allowed[$typePrefix]
		Out "  $typePrefix ($($objMap.Count)):"
		OutGroup $objMap $typePrefix
		Out ""
	}
}
else {
	Out "(no allowed rights)"
	Out ""
}

# Denied rights
if ($ShowDenied -and $denied.Count -gt 0) {
	Out "Denied rights:"
	Out ""
	foreach ($typePrefix in $denied.Keys) {
		$objMap = $denied[$typePrefix]
		Out "  $typePrefix ($($objMap.Count)):"
		OutGroup $objMap $typePrefix -isDenied
		Out ""
	}
}
elseif ($totalDenied -gt 0) {
	Out "Denied: $totalDenied rights (use -ShowDenied to list)"
	Out ""
}

# RLS summary
if ($rlsObjects.Count -gt 0) {
	Out "RLS: $($rlsObjects.Count) restrictions"
}

# Templates
if ($templates.Count -gt 0) {
	Out "Templates: $($templates -join ', ')"
}

Out ""
Out "---"
Out "Total: $totalAllowed allowed, $totalDenied denied"

# --- Pagination and output ---
$totalLines = $script:lines.Count
$lines = $script:lines

if ($Offset -gt 0) {
	if ($Offset -ge $totalLines) {
		Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
		exit 0
	}
	$lines = $lines[$Offset..($totalLines - 1)]
}

if ($Limit -gt 0 -and $lines.Count -gt $Limit) {
	$shown = $lines[0..($Limit - 1)]
	$remaining = $totalLines - $Offset - $Limit
	$shown += ""
	$shown += "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue."
	$lines = $shown
}

if ($OutFile) {
	if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
		$OutFile = Join-Path (Get-Location).Path $OutFile
	}
	$utf8 = New-Object System.Text.UTF8Encoding($true)
	[System.IO.File]::WriteAllLines($OutFile, $lines, $utf8)
	Write-Host "Output written to $OutFile"
} else {
	foreach ($l in $lines) { Write-Host $l }
}
