# mxl-info v1.5 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Position=0)]
	[Alias('Path')]
	[string]$TemplatePath,
	[string]$ProcessorName,
	[string]$TemplateName,
	[string]$SrcDir = "src",
	[ValidateSet("text", "json")]
	[string]$Format = "text",
	[switch]$WithText,
	[int]$MaxParams = 10,
	[int]$Limit = 150,
	[int]$Offset = 0
)

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

# --- Resolve template path ---

if (-not $TemplatePath) {
	if (-not $ProcessorName -or -not $TemplateName) {
		Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName"
		exit 1
	}
	$TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml"
}

if (-not (Test-Path $TemplatePath)) {
	Write-Error "File not found: $TemplatePath"
	exit 1
}

# --- Load XML ---

$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)

$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")

$root = $xmlDoc.DocumentElement

# --- Column sets ---

$columnSets = @()
$defaultColCount = 0

foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) {
	$sizeNode = $cols.SelectSingleNode("d:size", $nsMgr)
	$idNode = $cols.SelectSingleNode("d:id", $nsMgr)
	$size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 }

	if ($idNode) {
		$columnSets += @{ Id = $idNode.InnerText; Size = $size }
	} else {
		$defaultColCount = $size
	}
}

# --- Rows: collect row data ---

$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr)
$totalRows = $rowNodes.Count

$heightNode = $root.SelectSingleNode("d:height", $nsMgr)
$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { $totalRows }

# --- Named items ---

$namedAreas = @()
$namedDrawings = @()

foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) {
	$niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
	$name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText

	if ($niType -like "*NamedItemCells*") {
		$area = $ni.SelectSingleNode("d:area", $nsMgr)
		$areaType = $area.SelectSingleNode("d:type", $nsMgr).InnerText
		$beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText
		$endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText
		$beginCol = [int]$area.SelectSingleNode("d:beginColumn", $nsMgr).InnerText
		$endCol = [int]$area.SelectSingleNode("d:endColumn", $nsMgr).InnerText
		$colsId = $null
		$colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr)
		if ($colsIdNode) { $colsId = $colsIdNode.InnerText }

		$namedAreas += @{
			Name      = $name
			AreaType  = $areaType
			BeginRow  = $beginRow
			EndRow    = $endRow
			BeginCol  = $beginCol
			EndCol    = $endCol
			ColumnsID = $colsId
		}
	} elseif ($niType -like "*NamedItemDrawing*") {
		$drawId = $ni.SelectSingleNode("d:drawingID", $nsMgr).InnerText
		$namedDrawings += @{ Name = $name; DrawingID = $drawId }
	}
}

# --- Scan rows for parameters and text ---

# Build row index map: rowIndex -> XmlNode
$rowMap = @{}
foreach ($ri in $rowNodes) {
	$idx = [int]$ri.SelectSingleNode("d:index", $nsMgr).InnerText
	$rowMap[$idx] = $ri
}

function Get-CellData {
	param($rowNode, [System.Xml.XmlNamespaceManager]$ns, [bool]$includeText)

	$row = $rowNode.SelectSingleNode("d:row", $ns)
	if (-not $row) { return @() }

	$results = @()
	foreach ($cGroup in $row.SelectNodes("d:c", $ns)) {
		$cell = $cGroup.SelectSingleNode("d:c", $ns)
		if (-not $cell) { continue }

		$param = $cell.SelectSingleNode("d:parameter", $ns)
		$detail = $cell.SelectSingleNode("d:detailParameter", $ns)
		$tl = $cell.SelectSingleNode("d:tl", $ns)

		if ($param) {
			$entry = @{ Kind = "Parameter"; Value = $param.InnerText }
			if ($detail) { $entry.Detail = $detail.InnerText }
			$results += $entry
		}

		if ($tl) {
			$content = $tl.SelectSingleNode("v8:item/v8:content", $ns)
			if ($content -and $content.InnerText) {
				$text = $content.InnerText
				$isTemplate = $text -match '\[.+\]'

				if ($isTemplate) {
					# Always extract parameter names from [Param] placeholders
					# Skip numeric-only like [5] — these are footnote refs in legal forms
					foreach ($m in [regex]::Matches($text, '\[([^\]]+)\]')) {
						$val = $m.Groups[1].Value
						if ($val -notmatch '^\d+$') {
							$results += @{ Kind = "TemplateParam"; Value = $val }
						}
					}
					# Full template text only with -WithText
					if ($includeText) {
						$results += @{ Kind = "Template"; Value = $text }
					}
				} elseif ($includeText) {
					$results += @{ Kind = "Text"; Value = $text }
				}
			}
		}
	}
	return $results
}

function Get-AreaCellData {
	param(
		[hashtable]$area,
		[hashtable]$rowMap,
		[System.Xml.XmlNamespaceManager]$ns,
		[bool]$includeText
	)

	$params = @()
	$details = @()
	$texts = @()
	$templates = @()

	$startRow = $area.BeginRow
	$endRow = $area.EndRow
	if ($startRow -eq -1) { $startRow = 0 }
	if ($endRow -eq -1) { $endRow = $docHeight - 1 }

	for ($r = $startRow; $r -le $endRow; $r++) {
		if ($rowMap.ContainsKey($r)) {
			$cells = Get-CellData -rowNode $rowMap[$r] -ns $ns -includeText $includeText
			foreach ($c in $cells) {
				switch ($c.Kind) {
					"Parameter" {
						$params += $c.Value
						if ($c.Detail) { $details += "$($c.Value)->$($c.Detail)" }
					}
					"TemplateParam" { $params += "$($c.Value) [tpl]" }
					"Text"          { $texts += $c.Value }
					"Template"      { $templates += $c.Value }
				}
			}
		}
	}

	return @{ Params = $params; Details = $details; Texts = $texts; Templates = $templates }
}

# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
$namedAreas = $namedAreas | Sort-Object {
	if ($_.AreaType -eq "Columns") { $_.BeginCol } else { $_.BeginRow }
}, { $_.Name }

# Collect data for each area
$areaData = @()
$coveredRows = @{}

foreach ($area in $namedAreas) {
	$data = Get-AreaCellData -area $area -rowMap $rowMap -ns $nsMgr -includeText $WithText
	$areaData += @{
		Area      = $area
		Params    = $data.Params
		Details   = $data.Details
		Texts     = $data.Texts
		Templates = $data.Templates
	}

	# Track covered rows
	$sr = $area.BeginRow
	$er = $area.EndRow
	if ($sr -ne -1 -and $er -ne -1) {
		for ($r = $sr; $r -le $er; $r++) {
			$coveredRows[$r] = $true
		}
	}
}

# Find parameters outside named areas
$outsideParams = @()
$outsideDetails = @()
$outsideTexts = @()
$outsideTemplates = @()

foreach ($r in $rowMap.Keys | Sort-Object) {
	if (-not $coveredRows.ContainsKey($r)) {
		$cells = Get-CellData -rowNode $rowMap[$r] -ns $nsMgr -includeText $WithText
		foreach ($c in $cells) {
			switch ($c.Kind) {
				"Parameter" {
					$outsideParams += $c.Value
					if ($c.Detail) { $outsideDetails += "$($c.Value)->$($c.Detail)" }
				}
				"TemplateParam" { $outsideParams += "$($c.Value) [tpl]" }
				"Text"      { $outsideTexts += $c.Value }
				"Template"  { $outsideTemplates += $c.Value }
			}
		}
	}
}

# --- Counts ---

$mergeCount = $root.SelectNodes("d:merge", $nsMgr).Count
$drawingNodes = $root.SelectNodes("d:drawing", $nsMgr)
$drawingCount = $drawingNodes.Count

# --- Output ---

function Truncate-List {
	param([string[]]$items, [int]$max)
	if ($items.Count -le $max) {
		return ($items -join ", ")
	}
	$shown = ($items[0..($max - 1)] -join ", ")
	$remaining = $items.Count - $max
	return "$shown, ... (+$remaining)"
}

# Determine template name from path
$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath)))

if ($Format -eq "json") {
	$result = @{
		name        = $templateName
		rows        = $docHeight
		columns     = $defaultColCount
		columnSets  = @($columnSets)
		areas       = @()
		outsideParams = @($outsideParams)
		mergeCount  = $mergeCount
		drawingCount = $drawingCount
	}

	foreach ($ad in $areaData) {
		$areaObj = @{
			name      = $ad.Area.Name
			type      = $ad.Area.AreaType
			beginRow  = $ad.Area.BeginRow
			endRow    = $ad.Area.EndRow
			beginCol  = $ad.Area.BeginCol
			endCol    = $ad.Area.EndCol
			params    = @($ad.Params)
		}
		if ($ad.Area.ColumnsID) { $areaObj.columnsID = $ad.Area.ColumnsID }
		if ($WithText) {
			$areaObj.texts = @($ad.Texts)
			$areaObj.templates = @($ad.Templates)
		}
		$result.areas += $areaObj
	}

	if ($WithText) {
		$result.outsideTexts = @($outsideTexts)
		$result.outsideTemplates = @($outsideTemplates)
	}

	foreach ($nd in $namedDrawings) {
		$result.areas += @{
			name      = $nd.Name
			type      = "Drawing"
			drawingID = $nd.DrawingID
		}
	}

	$result | ConvertTo-Json -Depth 5
	exit 0
}

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 "не на поддержке" }
}

# --- Text format output ---

$lines = @()

$lines += "=== $templateName ==="
$support = Get-SupportStatusForPath $TemplatePath
if ($null -ne $support) { $lines += "Поддержка: $support" }
$lines += "  Rows: $docHeight, Columns: $defaultColCount"

if ($columnSets.Count -eq 0) {
	$lines += "  Column sets: 1 (default only)"
} else {
	$lines += "  Column sets: $($columnSets.Count + 1) (default=$defaultColCount cols + $($columnSets.Count) additional)"
	foreach ($cs in $columnSets) {
		$lines += "    $($cs.Id.Substring(0,8))...: $($cs.Size) cols"
	}
}

$lines += ""
$lines += "--- Named areas ---"

foreach ($ad in $areaData) {
	$a = $ad.Area
	$paramCount = $ad.Params.Count
	$rowRange = ""

	switch ($a.AreaType) {
		"Rows"      { $rowRange = "rows $($a.BeginRow)-$($a.EndRow)" }
		"Columns"   { $rowRange = "cols $($a.BeginCol)-$($a.EndCol)" }
		"Rectangle" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow), cols $($a.BeginCol)-$($a.EndCol)" }
	}

	$colsInfo = ""
	if ($a.ColumnsID) {
		$csSize = ""
		foreach ($cs in $columnSets) {
			if ($cs.Id -eq $a.ColumnsID) { $csSize = " $($cs.Size)cols"; break }
		}
		$colsInfo = " [colset$csSize]"
	}

	$paramInfo = "($paramCount params)"
	$nameStr = $a.Name.PadRight(25)
	$typeStr = $a.AreaType.PadRight(12)
	$lines += "  $nameStr $typeStr $rowRange  $paramInfo$colsInfo"
}

foreach ($nd in $namedDrawings) {
	$nameStr = $nd.Name.PadRight(25)
	$lines += "  $nameStr Drawing      drawingID=$($nd.DrawingID)"
}

# Detect intersection pairs (Rows + Columns areas that overlap)
$rowsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Rows" }
$colsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Columns" }
$intersections = @()
if ($rowsAreas -and $colsAreas) {
	foreach ($ra in $rowsAreas) {
		foreach ($ca in $colsAreas) {
			$intersections += "$($ra.Area.Name)|$($ca.Area.Name)"
		}
	}
}

if ($intersections.Count -gt 0) {
	$lines += ""
	$lines += "--- Intersections (use with GetArea) ---"
	foreach ($pair in $intersections) {
		$lines += "  $pair"
	}
}

# Parameters by area
$hasParams = ($areaData | Where-Object { $_.Params.Count -gt 0 }) -or ($outsideParams.Count -gt 0)

if ($hasParams) {
	$lines += ""
	$lines += "--- Parameters by area ---"
	foreach ($ad in $areaData) {
		if ($ad.Params.Count -gt 0) {
			$paramStr = Truncate-List -items $ad.Params -max $MaxParams
			$lines += "  $($ad.Area.Name): $paramStr"
			# Show detailParameters if any
			if ($ad.Details.Count -gt 0) {
				$detailStr = Truncate-List -items $ad.Details -max $MaxParams
				$lines += "    detail: $detailStr"
			}
		}
	}
	if ($outsideParams.Count -gt 0) {
		$paramStr = Truncate-List -items $outsideParams -max $MaxParams
		$lines += "  (outside areas): $paramStr"
		if ($outsideDetails.Count -gt 0) {
			$detailStr = Truncate-List -items $outsideDetails -max $MaxParams
			$lines += "    detail: $detailStr"
		}
	}
}

# WithText sections
if ($WithText) {
	$hasText = ($areaData | Where-Object { $_.Texts.Count -gt 0 -or $_.Templates.Count -gt 0 }) -or ($outsideTexts.Count -gt 0) -or ($outsideTemplates.Count -gt 0)

	if ($hasText) {
		$lines += ""
		$lines += "--- Text content ---"
		foreach ($ad in $areaData) {
			if ($ad.Texts.Count -gt 0 -or $ad.Templates.Count -gt 0) {
				$lines += "  $($ad.Area.Name):"
				if ($ad.Texts.Count -gt 0) {
					$textItems = $ad.Texts | ForEach-Object { "`"$_`"" }
					$textStr = Truncate-List -items $textItems -max $MaxParams
					$lines += "    Text: $textStr"
				}
				if ($ad.Templates.Count -gt 0) {
					$tplItems = $ad.Templates | ForEach-Object { "`"$_`"" }
					$tplStr = Truncate-List -items $tplItems -max $MaxParams
					$lines += "    Templates: $tplStr"
				}
			}
		}
		if ($outsideTexts.Count -gt 0 -or $outsideTemplates.Count -gt 0) {
			$lines += "  (outside areas):"
			if ($outsideTexts.Count -gt 0) {
				$textItems = $outsideTexts | ForEach-Object { "`"$_`"" }
				$textStr = Truncate-List -items $textItems -max $MaxParams
				$lines += "    Text: $textStr"
			}
			if ($outsideTemplates.Count -gt 0) {
				$tplItems = $outsideTemplates | ForEach-Object { "`"$_`"" }
				$tplStr = Truncate-List -items $tplItems -max $MaxParams
				$lines += "    Templates: $tplStr"
			}
		}
	}
}

$lines += ""
$lines += "--- Stats ---"
$lines += "  Merges: $mergeCount"
$lines += "  Drawings: $drawingCount"

# --- Truncation protection ---

$totalLines = $lines.Count

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 ($lines.Count -gt $Limit) {
	$shown = $lines[0..($Limit - 1)]
	foreach ($l in $shown) { Write-Host $l }
	$remaining = $totalLines - $Offset - $Limit
	Write-Host ""
	Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue."
} else {
	foreach ($l in $lines) { Write-Host $l }
}
