# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Mandatory)]
	[string]$ExtensionPath,

	[string]$ConfigPath,

	# Generation: logical name or path to .bsl. Optional in -Check/-Actualize (scope narrowing).
	[string]$ModulePath,

	[string]$MethodName,

	[ValidateSet("", "Before", "After", "Instead", "ModificationAndControl")]
	[string]$InterceptorType,

	# Batch modes over &ИзменениеИКонтроль of the extension (no generation):
	[switch]$Check,      # report drift only, no writes
	[switch]$Actualize   # actualize drifted controlled methods
)

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

# ============================================================================
# Helpers
# ============================================================================

$script:typeDirMap = @{
	"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
	"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
	"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
	"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
	"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
	"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
	"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
	"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
	"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
	"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
	"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
	"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
	"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
	"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
	"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
	"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
}

# InterceptorType -> Russian decorator keyword
$script:decoratorMap = @{
	"Before"="Перед"; "After"="После"; "Instead"="Вместо"
	"ModificationAndControl"="ИзменениеИКонтроль"
}

# Compute relative .bsl path segments from ModulePath (used for both ext and src)
function Get-ModuleRelPath {
	param([string]$modulePath)
	$parts = $modulePath.Split(".")
	if ($parts.Count -lt 2) {
		throw "Invalid ModulePath format: $modulePath. Expected: Type.Name.Module, Type.Name.Form.FormName or CommonModule.Name"
	}
	$objType = $parts[0]
	$objName = $parts[1]
	if (-not $script:typeDirMap.ContainsKey($objType)) {
		throw "Unknown object type: $objType"
	}
	$dirName = $script:typeDirMap[$objType]

	if ($objType -eq "CommonModule") {
		return @($dirName, $objName, "Ext", "Module.bsl")
	} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
		$formName = $parts[3]
		return @($dirName, $objName, "Forms", $formName, "Ext", "Form", "Module.bsl")
	} elseif ($parts.Count -ge 3) {
		$moduleName = $parts[2]
		$moduleFileName = switch ($moduleName) {
			"ObjectModule"    { "ObjectModule.bsl" }
			"ManagerModule"   { "ManagerModule.bsl" }
			"RecordSetModule" { "RecordSetModule.bsl" }
			"CommandModule"   { "CommandModule.bsl" }
			"ValueManagerModule" { "ValueManagerModule.bsl" }
			default           { "$moduleName.bsl" }
		}
		return @($dirName, $objName, "Ext", $moduleFileName)
	}
	throw "Invalid ModulePath format: $modulePath"
}

# Extract relative module path segments from a filesystem path to a .bsl,
# anchored on a known type directory (Catalogs/Documents/CommonModules/...).
function Get-RelPartsFromFilePath {
	param([string]$path)
	$segs = ($path -replace '\\', '/').Split('/') | Where-Object { $_ -ne '' }
	$anchors = $script:typeDirMap.Values | Select-Object -Unique
	for ($i = 0; $i -lt $segs.Count; $i++) {
		# case-sensitive: 1C type dirs are PascalCase (Catalogs/Documents/Tasks/…)
		if ($anchors -ccontains $segs[$i]) { return $segs[$i..($segs.Count - 1)] }
	}
	return $null
}

# Split text at top-level separator (respecting parens and string literals)
function Split-TopLevel {
	param([string]$text, [char]$sep = ',')
	$result = @()
	$depth = 0; $inStr = $false
	$sb = New-Object System.Text.StringBuilder
	for ($i = 0; $i -lt $text.Length; $i++) {
		$ch = $text[$i]
		if ($inStr) {
			[void]$sb.Append($ch)
			if ($ch -eq '"') { $inStr = $false }
			continue
		}
		if ($ch -eq '"') { $inStr = $true; [void]$sb.Append($ch); continue }
		if ($ch -eq '(') { $depth++; [void]$sb.Append($ch); continue }
		if ($ch -eq ')') { $depth--; [void]$sb.Append($ch); continue }
		if ($ch -eq $sep -and $depth -eq 0) { $result += $sb.ToString(); $sb = New-Object System.Text.StringBuilder; continue }
		[void]$sb.Append($ch)
	}
	$result += $sb.ToString()
	return $result
}

# Is a trimmed line a context-directive annotation?
function Test-ContextDirective {
	param([string]$trimmed)
	return $trimmed -match '^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$'
}

# Read a method signature starting at declaration line; returns @{ ParamsText; EndLineIdx }
function Read-Signature {
	param($lines, [int]$startIdx)
	$depth = 0; $inStr = $false; $openFound = $false
	$paramsSb = New-Object System.Text.StringBuilder
	for ($li = $startIdx; $li -lt $lines.Count; $li++) {
		$line = $lines[$li]
		for ($ci = 0; $ci -lt $line.Length; $ci++) {
			$ch = $line[$ci]
			if ($inStr) {
				if ($openFound) { [void]$paramsSb.Append($ch) }
				if ($ch -eq '"') { $inStr = $false }
				continue
			}
			if ($ch -eq '"') { $inStr = $true; if ($openFound) { [void]$paramsSb.Append($ch) }; continue }
			if ($ch -eq '(') {
				$depth++
				if (-not $openFound) { $openFound = $true } else { [void]$paramsSb.Append($ch) }
				continue
			}
			if ($ch -eq ')') {
				$depth--
				if ($depth -eq 0) { return @{ ParamsText = $paramsSb.ToString(); EndLineIdx = $li } }
				[void]$paramsSb.Append($ch)
				continue
			}
			if ($openFound) { [void]$paramsSb.Append($ch) }
		}
		if ($openFound -and $depth -ge 1) { [void]$paramsSb.Append("`r`n") }
	}
	return $null
}

# Compute effective condition of the current branch of an #Если frame
function Get-EffectiveCondition {
	param($frame)
	$conds = $frame.Conds
	$n = $conds.Count
	if ($frame.InElse) {
		$parts = @()
		foreach ($c in $conds) { $parts += "НЕ ($c)" }
		return ($parts -join " И ")
	}
	if ($n -eq 1) { return $conds[0] }
	$parts = @()
	for ($j = 0; $j -lt ($n - 1); $j++) { $parts += "НЕ ($($conds[$j]))" }
	$parts += $conds[$n - 1]
	return ($parts -join " И ")
}

# Extract the enclosing wrapper chain (regions + preprocessor) at a target line.
# Returns array outer->inner of @{ Kind='region'|'if'; Name=..; Cond=.. }
function Get-EnclosingChain {
	param($lines, [int]$targetIdx)
	$stack = New-Object System.Collections.ArrayList
	for ($i = 0; $i -lt $targetIdx; $i++) {
		$t = $lines[$i].Trim()
		if ($t -match '^#Область\s+(\S+)') {
			[void]$stack.Add(@{ Kind = 'region'; Name = $Matches[1] })
		} elseif ($t -match '^#КонецОбласти') {
			for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'region') { $stack.RemoveAt($k); break } }
		} elseif ($t -match '^#Если\s+(.+?)\s+Тогда') {
			[void]$stack.Add(@{ Kind = 'if'; Conds = @($Matches[1].Trim()); InElse = $false })
		} elseif ($t -match '^#ИначеЕсли\s+(.+?)\s+Тогда') {
			for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].Conds += $Matches[1].Trim(); $stack[$k].InElse = $false; break } }
		} elseif ($t -match '^#Иначе(\s|$)') {
			for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].InElse = $true; break } }
		} elseif ($t -match '^#КонецЕсли') {
			for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack.RemoveAt($k); break } }
		}
	}
	$chain = @()
	foreach ($f in $stack) {
		if ($f.Kind -eq 'region') { $chain += @{ Kind = 'region'; Name = $f.Name } }
		else { $chain += @{ Kind = 'if'; Cond = (Get-EffectiveCondition $f) } }
	}
	return ,$chain
}

# Extract a method from source .bsl lines. Returns $null if not found.
function Extract-Method {
	param($lines, [string]$methodName)
	$declRe = '^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + [regex]::Escape($methodName) + ')\s*\('
	for ($i = 0; $i -lt $lines.Count; $i++) {
		if ($lines[$i] -imatch $declRe) {
			$isAsync = [bool]$Matches[1]
			$keyword = $Matches[2]
			$canonical = $Matches[3]
			$isFunction = ($keyword -ieq "Функция")

			$sig = Read-Signature $lines $i
			if (-not $sig) { throw "Не удалось разобрать сигнатуру метода '$methodName'" }
			$sigEnd = $sig.EndLineIdx
			$paramsText = $sig.ParamsText

			# Parameter names (for ПродолжитьВызов)
			$paramNames = @()
			if ($paramsText.Trim().Length -gt 0) {
				foreach ($seg in (Split-TopLevel $paramsText)) {
					$s = $seg.Trim() -replace '^Знач\s+', ''
					if ($s -match '^([\w]+)') { $paramNames += $Matches[1] }
				}
			}

			# Body: from sigEnd+1 to matching Конец*
			$endRe = if ($isFunction) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' }
			$bodyStart = $sigEnd + 1
			$bodyEnd = -1
			for ($j = $bodyStart; $j -lt $lines.Count; $j++) {
				if ($lines[$j] -imatch $endRe) { $bodyEnd = $j; break }
			}
			if ($bodyEnd -lt 0) { throw "Не найден конец метода '$methodName'" }
			$bodyLines = @()
			for ($j = $bodyStart; $j -lt $bodyEnd; $j++) { $bodyLines += $lines[$j] }

			# Context directive (immediately above declaration)
			$context = ""
			if ($i -ge 1) {
				$prev = $lines[$i - 1].Trim()
				if (Test-ContextDirective $prev) { $context = $prev }
			}

			# Enclosing chain (regions + preprocessor)
			$chain = Get-EnclosingChain $lines $i

			return @{
				Canonical   = $canonical
				IsFunction  = $isFunction
				IsAsync     = $isAsync
				ParamsText  = $paramsText
				ParamNames  = $paramNames
				Context     = $context
				BodyLines   = $bodyLines
				Chain       = $chain
				DeclIdx     = $i
				SigEndIdx   = $sigEnd
				BodyEndIdx  = $bodyEnd
			}
		}
	}
	return $null
}

# Parse interceptors present in a module (type + method)
function Get-Interceptors {
	param($lines)
	$result = @()
	for ($i = 0; $i -lt $lines.Count; $i++) {
		$t = $lines[$i].Trim()
		if ($t -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
			$result += @{ Type = $Matches[1]; Method = $Matches[2]; Line = $i }
		}
	}
	return $result
}

# Parse declared procedure/function names in a module
function Get-ProcNames {
	param($lines)
	$names = @()
	foreach ($line in $lines) {
		if ($line -imatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') { $names += $Matches[1] }
	}
	return $names
}

# ============================================================================
# Build interceptor block (without enclosing wrappers)
# ============================================================================
function Build-InterceptorCore {
	param($method, [string]$interceptorType, [string]$interceptorName)

	$decoratorRu = $script:decoratorMap[$interceptorType]
	$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
	$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
	$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }

	$lines = @()
	if ($method.Context) { $lines += $method.Context }
	$lines += "&$decoratorRu(`"$($method.Canonical)`")"
	$lines += "$asyncPrefix$keyword $interceptorName($($method.ParamsText))"

	switch ($interceptorType) {
		"Before" {
			$lines += "`t// TODO: код перед вызовом оригинального метода"
		}
		"After" {
			$lines += "`t// TODO: код после вызова оригинального метода"
		}
		"Instead" {
			$namesJoined = ($method.ParamNames -join ", ")
			if ($method.IsFunction) {
				$lines += "`tРезультат = ПродолжитьВызов($namesJoined);"
				$lines += "`t// TODO: доработать поведение"
				$lines += "`tВозврат Результат;"
			} else {
				$lines += "`tПродолжитьВызов($namesJoined);"
				$lines += "`t// TODO: доработать поведение"
			}
		}
		"ModificationAndControl" {
			foreach ($bl in $method.BodyLines) { $lines += $bl }
		}
	}
	$lines += $endKeyword
	return $lines
}

# Wrap core with region/preprocessor lines, adding blank lines ("air") around
# each structural boundary. Empty chain -> core as-is.
function Build-WrappedBlock {
	param($chainArr, $core)
	$b = @()
	foreach ($w in $chainArr) {
		if ($w.Kind -eq 'region') { $b += "#Область $($w.Name)" } else { $b += "#Если $($w.Cond) Тогда" }
		$b += ""
	}
	$b += $core
	for ($c = $chainArr.Count - 1; $c -ge 0; $c--) {
		$b += ""
		if ($chainArr[$c].Kind -eq 'region') { $b += "#КонецОбласти" } else { $b += "#КонецЕсли" }
	}
	return $b
}

# ============================================================================
# Resync helpers (&ИзменениеИКонтроль)
# ============================================================================

function Get-Normalized {
	param([string]$line)
	return (($line -replace '\s+', ' ').Trim())
}

# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
function Get-ControlKey {
	param($lines)
	return (@($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join "`n")
}

# Parameter count of a signature params text. The platform compares only the number of
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
function Get-ParamCount {
	param([string]$paramsText)
	if ([string]::IsNullOrWhiteSpace($paramsText)) { return 0 }
	return @(Split-TopLevel $paramsText | Where-Object { $_.Trim() -ne '' }).Count
}

# Reconstruct v1 body and edit ops from a marked body
function Parse-MarkedBody {
	param($bodyLines)
	$v1 = @()          # reconstructed original lines
	$ops = @()         # edit operations
	$i = 0
	while ($i -lt $bodyLines.Count) {
		$t = $bodyLines[$i].Trim()
		if ($t -eq '#Вставка') {
			$ins = @()
			$i++
			while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецВставки') { $ins += $bodyLines[$i]; $i++ }
			$i++  # skip #КонецВставки
			$ops += @{ Kind = 'insert'; After = ($v1.Count - 1); Lines = $ins }
		} elseif ($t -eq '#Удаление') {
			$startIdx = $v1.Count
			$i++
			$del = @()
			while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецУдаления') { $del += $bodyLines[$i]; $v1 += $bodyLines[$i]; $i++ }
			$i++  # skip #КонецУдаления
			$ops += @{ Kind = 'delete'; Start = $startIdx; End = ($v1.Count - 1); Lines = $del }
		} else {
			$v1 += $bodyLines[$i]
			$i++
		}
	}
	return @{ V1 = $v1; Ops = $ops }
}

# Find unique normalized index of a line in v2; -1 if absent or ambiguous
function Find-UniqueIndex {
	param($v2norm, [string]$key)
	$found = -1
	for ($k = 0; $k -lt $v2norm.Count; $k++) {
		if ($v2norm[$k] -eq $key) {
			if ($found -ge 0) { return -1 }  # ambiguous
			$found = $k
		}
	}
	return $found
}

# Find unique contiguous run of normalized lines in v2; returns start index or -1
function Find-UniqueRun {
	param($v2norm, $keys)
	if ($keys.Count -eq 0) { return -1 }
	$found = -1
	for ($k = 0; $k -le ($v2norm.Count - $keys.Count); $k++) {
		$match = $true
		for ($m = 0; $m -lt $keys.Count; $m++) {
			if ($v2norm[$k + $m] -ne $keys[$m]) { $match = $false; break }
		}
		if ($match) {
			if ($found -ge 0) { return -1 }  # ambiguous
			$found = $k
		}
	}
	return $found
}

# A "significant" line carries code: non-empty and not a whole-line comment (//).
# Blank lines and comment-only lines are cosmetic — transparent to anchor/absorption matching.
function Test-Significant {
	param([string]$normLine)
	return ($normLine -ne '' -and -not $normLine.StartsWith('//'))
}

# Project normalized lines to significant-only. Returns @{ Sig = @(values); Map = @(orig indices) }.
function Get-SignificantProjection {
	param($norm)
	$sig = @(); $map = @()
	for ($i = 0; $i -lt $norm.Count; $i++) {
		if (Test-Significant $norm[$i]) { $sig += $norm[$i]; $map += $i }
	}
	return @{ Sig = $sig; Map = $map }
}

# True if the normalized run `keys` sits in `hay` starting exactly at index `at` (contiguous, in-bounds).
# Used to detect an insert whose payload the vendor already placed at the anchor (pereneseno v osnovnuyu).
function Test-RunAt {
	param($hay, $keys, [int]$at)
	if ($keys.Count -eq 0) { return $false }
	if ($at -lt 0 -or ($at + $keys.Count) -gt $hay.Count) { return $false }
	for ($m = 0; $m -lt $keys.Count; $m++) { if ($hay[$at + $m] -ne $keys[$m]) { return $false } }
	return $true
}

# True if a deletion is already applied in v2: its before/after context lines are now adjacent
# (nothing left between them), or the missing side sits at a body boundary. $null = boundary side.
function Test-DeleteAbsorbed {
	param($v2norm, $beforeCtx, $afterCtx)
	if ($null -ne $beforeCtx -and $null -ne $afterCtx) {
		for ($i = 0; $i -lt ($v2norm.Count - 1); $i++) {
			if ($v2norm[$i] -eq $beforeCtx -and $v2norm[$i + 1] -eq $afterCtx) { return $true }
		}
		return $false
	}
	if ($null -eq $beforeCtx -and $null -ne $afterCtx) { return ($v2norm.Count -gt 0 -and $v2norm[0] -eq $afterCtx) }
	if ($null -ne $beforeCtx -and $null -eq $afterCtx) { return ($v2norm.Count -gt 0 -and $v2norm[$v2norm.Count - 1] -eq $beforeCtx) }
	return $false
}

# Exact two-sided resolution over the given array.
# Returns index to "insert after" (-1 = top of body), or $null if ambiguous/conflict.
function Resolve-InsertionPointExact {
	param($v2norm, $beforeLines, $afterLines)
	$nb = $beforeLines.Count; $na = $afterLines.Count

	# Tier A: adjacent pair (before[-1] immediately followed by after[0] in v2), widening symmetrically
	if ($nb -ge 1 -and $na -ge 1) {
		$cands = @()
		for ($k = 0; $k -lt ($v2norm.Count - 1); $k++) {
			if ($v2norm[$k] -eq $beforeLines[$nb - 1] -and $v2norm[$k + 1] -eq $afterLines[0]) { $cands += $k }
		}
		if ($cands.Count -eq 1) { return $cands[0] }
		if ($cands.Count -gt 1) {
			$w = 1
			while ($cands.Count -gt 1 -and ($w -lt $nb -or $w -lt $na)) {
				$w++
				$filtered = @()
				foreach ($k in $cands) {
					$ok = $true
					if ($w -le $nb) {
						if (($k - ($w - 1)) -lt 0 -or $v2norm[$k - ($w - 1)] -ne $beforeLines[$nb - $w]) { $ok = $false }
					}
					if ($ok -and $w -le $na) {
						if (($k + $w) -ge $v2norm.Count -or $v2norm[$k + $w] -ne $afterLines[$w - 1]) { $ok = $false }
					}
					if ($ok) { $filtered += $k }
				}
				if ($filtered.Count -eq 0) { break }
				$cands = $filtered
			}
			if ($cands.Count -eq 1) { return $cands[0] }
			return $null
		}
	}

	# Tier B: one side changed -> single-side uniqueness
	if ($nb -ge 1) {
		$bk = Find-UniqueIndex $v2norm $beforeLines[$nb - 1]
		if ($bk -ge 0) { return $bk }
	}
	if ($na -ge 1) {
		$ak = Find-UniqueIndex $v2norm $afterLines[0]
		if ($ak -ge 0) { return ($ak - 1) }
	}
	return $null
}

# Resolve where an insertion lands in v2. Exact first (comments/blanks included — keeps the insert's
# position relative to a stable comment); on failure, retry on significant lines only (transparent to
# vendor-added blanks/comments) and map back to a full-v2 index. Returns "insert after" idx or $null.
function Resolve-InsertionPoint {
	param($v2norm, $beforeLines, $afterLines)
	$k = Resolve-InsertionPointExact $v2norm $beforeLines $afterLines
	if ($null -ne $k) { return $k }
	$proj = Get-SignificantProjection $v2norm
	$bs = @($beforeLines | Where-Object { Test-Significant $_ })
	$as = @($afterLines | Where-Object { Test-Significant $_ })
	$ksig = Resolve-InsertionPointExact $proj.Sig $bs $as
	if ($null -eq $ksig) { return $null }
	if ($ksig -lt 0) { return -1 }
	return $proj.Map[$ksig]
}

# Truncate a line for compact summary output
function Get-Truncated {
	param([string]$s, [int]$n = 60)
	$t = $s.Trim()
	if ($t.Length -gt $n) { return $t.Substring(0, $n) + "…" }
	return $t
}

# Reverse of typeDirMap (plural dir -> singular type) for logical id display
$script:dirToType = @{
	"Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "CommonModules"="CommonModule"
	"Reports"="Report"; "DataProcessors"="DataProcessor"; "ExchangePlans"="ExchangePlan"
	"ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes"
	"ChartsOfCalculationTypes"="ChartOfCalculationTypes"; "BusinessProcesses"="BusinessProcess"
	"Tasks"="Task"; "InformationRegisters"="InformationRegister"
	"AccumulationRegisters"="AccumulationRegister"; "AccountingRegisters"="AccountingRegister"
	"CalculationRegisters"="CalculationRegister"
}

# Rel-path segments (relative to ExtensionPath) -> logical ModulePath (Type.Name.Module / CommonModule.Name)
function Get-ModulePathFromRel {
	param($relParts)
	$dir0 = $relParts[0]; $name = $relParts[1]
	$typ = if ($script:dirToType.ContainsKey($dir0)) { $script:dirToType[$dir0] } else { $dir0 }
	if ($dir0 -eq "CommonModules") { return "CommonModule.$name" }
	if ($relParts.Count -ge 7 -and $relParts[2] -eq "Forms") { return "$typ.$name.Form.$($relParts[3])" }
	$mod = $relParts[$relParts.Count - 1]
	if ($mod.EndsWith(".bsl")) { $mod = $mod.Substring(0, $mod.Length - 4) }
	return "$typ.$name.$mod"
}

function Get-RelPartsUnder {
	param([string]$root, [string]$fullPath)
	$r = [IO.Path]::GetFullPath($root).TrimEnd('\', '/')
	$f = [IO.Path]::GetFullPath($fullPath)
	$rel = $f.Substring($r.Length).TrimStart('\', '/')
	return (($rel -replace '\\', '/').Split('/') | Where-Object { $_ -ne '' })
}

function Get-ResyncConflictReason {
	param($disputed)
	$kinds = @($disputed | ForEach-Object { $_.Kind } | Select-Object -Unique)
	$parts = @()
	if ($kinds -contains 'insert') { $parts += 'якорь вставки изменён' }
	if ($kinds -contains 'delete') { $parts += 'удаляемое исчезло' }
	return ($parts -join '; ')
}

# Write per-method conflict folder: conflict.md + base/local/remote
function Write-ConflictFolder {
	param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, $enc)
	if (-not (Test-Path $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null }
	[IO.File]::WriteAllText((Join-Path $folder 'base.bsl'), (($v1 -join "`r`n") + "`r`n"), $enc)
	[IO.File]::WriteAllText((Join-Path $folder 'local.bsl'), (($markedBody -join "`r`n") + "`r`n"), $enc)
	[IO.File]::WriteAllText((Join-Path $folder 'remote.bsl'), (($v2 -join "`r`n") + "`r`n"), $enc)
	$md = @()
	$md += "# $methodId"
	$md += "Править: $extBsl"
	$md += "Метод:   $existingName (&ИзменениеИКонтроль(`"$($method.Canonical)`"))"
	$md += "Причина: $(Get-ResyncConflictReason $disputed)"
	$md += ""
	$md += "## Не размещено — перенести вручную"
	$cn = 0
	foreach ($d in $disputed) {
		$cn++
		$md += ""
		if ($d.Kind -eq 'insert') {
			$md += "### Конфликт №$cn — вставка"
			$md += "Как блок стоял в вашей версии (local):"
			if ($d.Before -and $d.Before.Count -gt 0) { foreach ($l in $d.Before) { $md += $l } }
			$md += "#Вставка"; foreach ($l in $d.Lines) { $md += $l }; $md += "#КонецВставки"
			if ($d.After -and $d.After.Count -gt 0) { foreach ($l in $d.After) { $md += $l } }
			$md += ""
			$md += "Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)."
			$md += "В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №$cn] — найди по ней."
			$md += "Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM)."
		} else {
			$md += "### Конфликт №$cn — удаление"
			$md += "Строки для удаления не найдены в новом оригинале (изменились/исчезли):"
			foreach ($l in $d.Lines) { $md += "  - $($l.Trim())" }
			$md += "В модуле расширения помечено меткой // [РЕСИНК-КОНФЛИКТ №$cn]."
		}
	}
	$md += ""
	$md += "## Дифф base→remote (что изменилось в оригинале)"
	foreach ($l in $v1) { if ($v2norm -notcontains (Get-Normalized $l)) { $md += "- $l" } }
	foreach ($l in $v2) { if ($v1norm -notcontains (Get-Normalized $l)) { $md += "+ $l" } }
	$md += ""
	$md += "Рядом: base.bsl / local.bsl / remote.bsl"
	[IO.File]::WriteAllText((Join-Path $folder 'conflict.md'), (($md -join "`r`n") + "`r`n"), $enc)
}

# Resync one &ИзменениеИКонтроль interceptor. Returns status hashtable.
# ReportOnly: classify without writing. Otherwise: apply to module (+ conflict folder on disputes).
function Invoke-Resync {
	param($extBsl, $extLines, $dup, $method, [string]$logicalModule, [string]$conflictFolder, [switch]$ReportOnly, $enc)

	$methodId = "$logicalModule.$($method.Canonical)"
	$decLine = $dup.Line
	$sigLineIdx = $decLine + 1
	if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') {
		return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' }
	}
	$existingName = $Matches[1]
	$sig = Read-Signature $extLines $sigLineIdx
	if (-not $sig) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру' } }
	$sigEnd = $sig.EndLineIdx
	$isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b')
	$endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' }
	$blockEnd = -1
	for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } }
	if ($blockEnd -lt 0) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не найден конец перехватчика' } }

	$markedBody = @()
	for ($j = $sigEnd + 1; $j -lt $blockEnd; $j++) { $markedBody += $extLines[$j] }

	$parsed = Parse-MarkedBody $markedBody
	$v1 = $parsed.V1
	$ops = $parsed.Ops
	$v2 = $method.BodyLines
	$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
	$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })

	# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
	# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
	$extParamCount = Get-ParamCount $sig.ParamsText
	$srcParamCount = Get-ParamCount $method.ParamsText
	$paramsDrift = ($extParamCount -ne $srcParamCount)
	$paramsReason = if ($paramsDrift) { "список параметров: в оригинале $srcParamCount, в перехватчике $extParamCount" } else { '' }

	if (-not $paramsDrift -and [string]::Equals((Get-ControlKey $v1), (Get-ControlKey $v2), 'Ordinal')) {
		return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
	}

	$insertTop = @(); $insertAfter = @{}; $delStart = @{}; $delEnd = @{}; $disputed = @(); $transferred = @(); $absorbed = @(); $absorbedNotes = @()
	$v2proj = Get-SignificantProjection $v2norm
	foreach ($op in $ops) {
		if ($op.Kind -eq 'insert') {
			$beforeLines = @()
			if ($op.After -ge 0) { $bs = [Math]::Max(0, $op.After - 2); for ($m = $bs; $m -le $op.After; $m++) { $beforeLines += $v1norm[$m] } }
			$afterLines = @()
			$ae = [Math]::Min($v1norm.Count - 1, $op.After + 3)
			for ($m = $op.After + 1; $m -le $ae; $m++) { $afterLines += $v1norm[$m] }
			$k = Resolve-InsertionPoint $v2norm $beforeLines $afterLines
			# Payload already in the new original -> change carried into the main config.
			# Match on significant lines only, so vendor-added blanks/comments don't hide it.
			$payloadSig = @(($op.Lines | ForEach-Object { Get-Normalized $_ }) | Where-Object { Test-Significant $_ })
			$isAbsorbed = $false
			if ($payloadSig.Count -gt 0) {
				if ($null -eq $k) { $isAbsorbed = (Find-UniqueRun $v2proj.Sig $payloadSig) -ge 0 }
				else { $sigStart = @($v2proj.Map | Where-Object { $_ -le $k }).Count; $isAbsorbed = Test-RunAt $v2proj.Sig $payloadSig $sigStart }
			}
			if ($isAbsorbed) {
				$absorbed += @{ Kind = 'insert' }
				foreach ($pl in $op.Lines) { if ((Get-Normalized $pl).StartsWith('//')) { $absorbedNotes += $pl.Trim() } }
			}
			elseif ($null -eq $k) {
				$dbefore = @(); if ($op.After -ge 0) { $bz = [Math]::Max(0, $op.After - 2); for ($z = $bz; $z -le $op.After; $z++) { $dbefore += $v1[$z] } }
				$dafter = @(); $az = [Math]::Min($v1.Count - 1, $op.After + 3); for ($z = $op.After + 1; $z -le $az; $z++) { $dafter += $v1[$z] }
				$disputed += @{ Kind = 'insert'; Lines = $op.Lines; Before = $dbefore; After = $dafter }
			}
			elseif ($k -lt 0) { $insertTop += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
			else { if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }; $insertAfter[$k] += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
		} else {
			$keys = @(); for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
			$p = Find-UniqueRun $v2norm $keys
			if ($p -ge 0) { $delStart[$p] = $true; $delEnd[$p + $keys.Count - 1] = $true; $transferred += @{ Kind = 'delete' } }
			else {
				# Nearest significant neighbours around the deleted block; adjacency in the significant
				# projection means the block is already cut (blanks/comments left behind don't matter).
				$delBeforeCtx = $null; for ($z = $op.Start - 1; $z -ge 0; $z--) { if (Test-Significant $v1norm[$z]) { $delBeforeCtx = $v1norm[$z]; break } }
				$delAfterCtx = $null; for ($z = $op.End + 1; $z -lt $v1norm.Count; $z++) { if (Test-Significant $v1norm[$z]) { $delAfterCtx = $v1norm[$z]; break } }
				if (Test-DeleteAbsorbed $v2proj.Sig $delBeforeCtx $delAfterCtx) { $absorbed += @{ Kind = 'delete' } }
				else { $disputed += @{ Kind = 'delete'; Lines = $op.Lines } }
			}
		}
	}

	if ($ReportOnly) {
		$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
		if ($paramsDrift -and $st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { $st = 'ДРЕЙФ' }
		$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
		if ($paramsDrift) { $rsn = if ($rsn) { "$paramsReason; $rsn" } else { $paramsReason } }
		return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
	}

	# assemble new marked body
	$newBody = @()
	foreach ($blk in $insertTop) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" }
	for ($k = 0; $k -lt $v2.Count; $k++) {
		if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" }
		$newBody += $v2[$k]
		if ($delEnd.ContainsKey($k)) { $newBody += "#КонецУдаления" }
		if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } }
	}
	if ($disputed.Count -gt 0) {
		$newBody += "`t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе)."
		$cn = 0
		foreach ($d in $disputed) {
			$cn++
			if ($d.Kind -eq 'insert') {
				$newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] вставка — исходный якорь изменён в новом оригинале."
				$newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки"
			}
			else {
				$newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] удаление — строки не найдены в новом оригинале:"
				foreach ($l in $d.Lines) { $newBody += ("`t// " + $l.Trim()) }
			}
		}
	}
	$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
	$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
	$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
	$newBlock = @()
	if ($method.Context) { $newBlock += $method.Context }
	$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")"
	$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
	$newBlock += $newBody
	$newBlock += $endKeyword

	$blockStart = $decLine
	if ($decLine -ge 1 -and (Test-ContextDirective $extLines[$decLine - 1].Trim())) { $blockStart = $decLine - 1 }
	$out = @()
	for ($j = 0; $j -lt $blockStart; $j++) { $out += $extLines[$j] }
	foreach ($l in $newBlock) { $out += $l }
	for ($j = $blockEnd + 1; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
	[IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)

	$conflictDir = $null
	if ($disputed.Count -gt 0) {
		$conflictDir = $conflictFolder
		Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $enc
	}
	$status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'АКТУАЛИЗИРОВАН' }
	$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($status -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации — перехватчик можно удалить' } else { '' }
	return @{ Id = $methodId; Status = $status; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; ConflictDir = $conflictDir; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
}

# Write run-root index.md (only if there are conflicts). Returns run-root or $null.
function Write-ResyncIndex {
	param($runRoot, $results, [string]$extName, [string]$configPath, [string]$verb, $enc)
	$conflicts = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' })
	if ($conflicts.Count -eq 0) { return $null }
	if (-not (Test-Path $runRoot)) { New-Item -ItemType Directory -Path $runRoot -Force | Out-Null }
	$total = $results.Count
	$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
	$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
	$lines = @()
	$lines += "[$verb] $extName -> $configPath"
	$lines += "Итог: $actual/$total актуальны · актуализировано: $upd · конфликтов: $($conflicts.Count)"
	$lines += ""
	$lines += "Конфликты — править .bsl расширения:"
	foreach ($c in $conflicts) {
		$lines += "  ЧАСТИЧНО  $($c.Id)"
		$lines += "            -> $($c.ExtBsl)   ($($c.Reason))"
	}
	[IO.File]::WriteAllText((Join-Path $runRoot 'index.md'), (($lines -join "`r`n") + "`r`n"), $enc)
	return $runRoot
}

# ============================================================================
# Main
# ============================================================================

# --- Resolve extension path ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath }
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }

# --- Пометка расширенного свойства (<xr:PropertyState>) ---
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
function Get-FormatRank([string]$ver) {
	if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
	return 0
}

function Detect-FormatVersion([string]$dir) {
	$d = $dir
	while ($d) {
		# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
		# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
		$extPath = "$d.xml"
		if (Test-Path $extPath) {
			$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
			$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
			if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
		}
		$cfgPath = Join-Path $d "Configuration.xml"
		if (Test-Path $cfgPath) {
			$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
			# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
			# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
			$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
			if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
		}
		$parent = Split-Path $d -Parent
		if ($parent -eq $d) { break }
		$d = $parent
	}
	return "2.17"
}

function Build-PropertyStateXml {
	param([string]$propertyName, [string]$indent)

	$sb = New-Object System.Text.StringBuilder
	$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
	$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
	$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
	$sb.Append("${indent}</xr:PropertyState>") | Out-Null
	return $sb.ToString()
}

function Set-PropertyStateFlag {
	param([string]$objFile, [string]$propertyName, [string]$formatVersion)

	if ((Get-FormatRank $formatVersion) -lt 219) { return }
	if (-not (Test-Path $objFile)) { return }

	$enc = New-Object System.Text.UTF8Encoding($true)
	$text = [System.IO.File]::ReadAllText($objFile, $enc)
	$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }

	# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
	# но они лежат ниже, внутри <ChildObjects>.
	$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
	$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')

	if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
		$ind = $empty.Groups[1].Value
		$block = Build-PropertyStateXml $propertyName ($ind + "`t")
		$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
		$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
	} elseif ($open.Success) {
		if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
		$ind = $open.Groups[1].Value
		$block = Build-PropertyStateXml $propertyName ($ind + "`t")
		# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
		$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
		$text = $text.Insert($closeAt, "${block}${nl}")
	} else {
		return
	}

	[System.IO.File]::WriteAllText($objFile, $text, $enc)
}

# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
function Get-ModuleFlagTarget {
	param([string[]]$relParts, [string]$extRoot)

	if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
	$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
	return @{
		File     = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
		Property = $prop
	}
}

# --- Read NamePrefix ---
$cfgDoc = New-Object System.Xml.XmlDocument
$cfgDoc.PreserveWhitespace = $false
$cfgDoc.Load($cfgFile)
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
$extNameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $cfgNs) } else { $null }
$extName = if ($extNameNode -and $extNameNode.InnerText) { $extNameNode.InnerText } else { "Расширение" }

# ============================================================================
# Batch modes: -Check / -Actualize over &ИзменениеИКонтроль of the extension
# ============================================================================
if ($Check -or $Actualize) {
	if ($Check -and $Actualize) { Write-Error "Укажите либо -Check, либо -Actualize, не оба."; exit 1 }
	if ([string]::IsNullOrEmpty($ConfigPath)) { Write-Error "Для -Check/-Actualize нужен -ConfigPath (сверка с исходником)."; exit 1 }
	if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path (Get-Location).Path $ConfigPath }
	if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
	if (-not (Test-Path (Join-Path $ConfigPath "Configuration.xml"))) { Write-Error "Configuration.xml не найден в конфигурации-источнике: $ConfigPath"; exit 1 }

	$enc = New-Object System.Text.UTF8Encoding($true)
	$reportOnly = [bool]$Check
	$verb = if ($Check) { "КОНТРОЛЬ" } else { "АКТУАЛИЗАЦИЯ" }

	# target .bsl set: -ModulePath → один модуль; иначе — все .bsl расширения
	$targetBsls = @()
	if (-not [string]::IsNullOrEmpty($ModulePath)) {
		$relParts = Get-ModuleRelPath $ModulePath
		$mb = $ExtensionPath; foreach ($p in $relParts) { $mb = Join-Path $mb $p }
		if (Test-Path $mb) { $targetBsls += $mb }
	} else {
		$targetBsls = @(Get-ChildItem -Path $ExtensionPath -Recurse -Filter *.bsl -File | ForEach-Object { $_.FullName })
	}

	$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\\/:*?"<>|]', '_'))
	if ($Actualize -and (Test-Path $runRoot)) { Remove-Item $runRoot -Recurse -Force -ErrorAction SilentlyContinue }

	$results = @()
	foreach ($tb in $targetBsls) {
		$scan = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
		$mnames = @(Get-Interceptors $scan | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' } | ForEach-Object { $_.Method })
		if (-not [string]::IsNullOrEmpty($MethodName)) { $mnames = @($mnames | Where-Object { $_ -ieq $MethodName }) }
		$mnames = @($mnames | Select-Object -Unique)
		if ($mnames.Count -eq 0) { continue }
		$rel = Get-RelPartsUnder $ExtensionPath $tb
		$logicalModule = Get-ModulePathFromRel $rel
		$srcBsl = $ConfigPath; foreach ($p in $rel) { $srcBsl = Join-Path $srcBsl $p }
		$relJoined = ($rel -join '\') -replace '\.bsl$', ''
		foreach ($mname in $mnames) {
			$mid = "$logicalModule.$mname"
			if (-not (Test-Path $srcBsl)) { $results += @{ Id = $mid; Status = 'ИСТОЧНИК-НЕ-НАЙДЕН'; ExtBsl = $tb }; continue }
			$srcL = [IO.File]::ReadAllLines($srcBsl, [Text.Encoding]::UTF8)
			$m = Extract-Method $srcL $mname
			if (-not $m) { $results += @{ Id = $mid; Status = 'МЕТОД-ИСЧЕЗ'; ExtBsl = $tb }; continue }
			# fresh read per method (writes shift line numbers)
			$tbLines = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
			$ic = @(Get-Interceptors $tbLines | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' -and $_.Method -ieq $mname })[0]
			if (-not $ic) { continue }
			$folder = Join-Path $runRoot (Join-Path $relJoined $m.Canonical)
			$results += (Invoke-Resync $tb $tbLines $ic $m $logicalModule $folder -ReportOnly:$reportOnly $enc)
		}
	}

	# --- report ---
	$total = $results.Count
	$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
	Write-Host "[$verb] $extName -> $ConfigPath   (на контроле: $total)"
	$listed = @($results | Where-Object { $_.Status -ne 'АКТУАЛЕН' })
	foreach ($p in $listed) {
		$line = "  {0,-22} {1}" -f $p.Status, $p.Id
		if ($p.Reason) { $line += "   $($p.Reason)" }
		Write-Host $line
		if ($p.AbsorbedNotes) { foreach ($n in $p.AbsorbedNotes) { Write-Host "     ⚠ комментарий не перенесён (код в основной конфигурации): $n" } }
	}
	$transf = @($results | Where-Object { $_.Status -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' }).Count
	if ($Check) {
		$drift = @($results | Where-Object { $_.Status -eq 'ДРЕЙФ' }).Count
		$confl = @($results | Where-Object { $_.Status -eq 'КОНФЛИКТ' }).Count
		$gone = @($results | Where-Object { $_.Status -in @('МЕТОД-ИСЧЕЗ', 'ИСТОЧНИК-НЕ-НАЙДЕН') }).Count
		Write-Host "Итог: $actual/$total актуальны · дрейф: $drift · конфликтов: $confl · перенесено в основную: $transf · внимания: $gone"
		if (($drift + $confl + $gone) -gt 0) { Write-Host "Починить: /cfe-patch-method -Actualize -ExtensionPath $ExtensionPath -ConfigPath $ConfigPath"; exit 1 }
		elseif ($transf -gt 0) { Write-Host "Перенесённые в основную конфигурацию правки подчистит: /cfe-patch-method -Actualize -ExtensionPath $ExtensionPath -ConfigPath $ConfigPath"; exit 0 }
		else { exit 0 }
	} else {
		$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
		$part = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' }).Count
		Write-Host "Итог: $actual/$total актуальны · актуализировано: $upd · частично: $part · перенесено в основную: $transf"
		$idx = Write-ResyncIndex $runRoot $results $extName $ConfigPath $verb $enc
		if ($idx) { Write-Host "Merge-воркспейс конфликтов (см. index.md): $idx" }
		exit 0
	}
}

# --- Generation mode: require ModulePath + MethodName + InterceptorType ---
if ([string]::IsNullOrEmpty($ModulePath) -or [string]::IsNullOrEmpty($MethodName) -or [string]::IsNullOrEmpty($InterceptorType)) {
	Write-Error "Нужны -ModulePath, -MethodName, -InterceptorType (генерация перехватчика). Для проверки/актуализации контролируемых методов используйте -Check или -Actualize."
	exit 1
}

# --- Resolve module file paths (ModulePath = logical name OR path to a .bsl) ---
$hasConfigPath = -not [string]::IsNullOrEmpty($ConfigPath)
if ($hasConfigPath) {
	if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path (Get-Location).Path $ConfigPath }
	if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
}

$isFilePath = ($ModulePath -match '[\\/]') -or ($ModulePath -match '\.bsl$')

if ($isFilePath) {
	# ModulePath is a filesystem path to the module .bsl
	$mpAbs = if ([System.IO.Path]::IsPathRooted($ModulePath)) { $ModulePath } else { Join-Path (Get-Location).Path $ModulePath }
	$relParts = Get-RelPartsFromFilePath $ModulePath
	if (-not $relParts) { Write-Error "Не удалось определить объект по пути модуля: $ModulePath`n(нет распознаваемой типовой папки — Catalogs/Documents/CommonModules/…)"; exit 1 }
	$extBsl = $ExtensionPath
	foreach ($p in $relParts) { $extBsl = Join-Path $extBsl $p }
	if ($hasConfigPath) {
		$srcBsl = $ConfigPath
		foreach ($p in $relParts) { $srcBsl = Join-Path $srcBsl $p }
	} else {
		# Guard: a path under the extension itself is not a valid source
		$extRootAbs = [System.IO.Path]::GetFullPath($ExtensionPath).TrimEnd('\','/')
		$mpFull = [System.IO.Path]::GetFullPath($mpAbs)
		if ($mpFull.StartsWith($extRootAbs, [System.StringComparison]::OrdinalIgnoreCase)) {
			Write-Error "Путь модуля указывает внутрь расширения, а не на источник. Укажите путь к модулю-источнику или -ConfigPath."; exit 1
		}
		$srcBsl = $mpAbs
	}
} else {
	# ModulePath is a logical name — ConfigPath required
	if (-not $hasConfigPath) { Write-Error "Не указан -ConfigPath. Укажите путь к исходникам конфигурации или передайте путь к файлу модуля в -ModulePath."; exit 1 }
	if (-not (Test-Path (Join-Path $ConfigPath "Configuration.xml"))) { Write-Error "Configuration.xml не найден в конфигурации-источнике: $ConfigPath"; exit 1 }
	$relParts = Get-ModuleRelPath $ModulePath
	$extBsl = $ExtensionPath
	foreach ($p in $relParts) { $extBsl = Join-Path $extBsl $p }
	$srcBsl = $ConfigPath
	foreach ($p in $relParts) { $srcBsl = Join-Path $srcBsl $p }
}

if (-not (Test-Path $srcBsl)) { Write-Error "Модуль-источник не найден: $srcBsl`n(проверьте ModulePath и ConfigPath)"; exit 1 }

# --- Extract original method ---
$srcLines = [System.IO.File]::ReadAllLines($srcBsl, [System.Text.Encoding]::UTF8)
$method = Extract-Method $srcLines $MethodName
if (-not $method) { Write-Error "Метод '$MethodName' не найден в модуле-источнике: $srcBsl"; exit 1 }

# --- Guard: functions cannot use Before/After ---
if ($method.IsFunction -and ($InterceptorType -eq "Before" -or $InterceptorType -eq "After")) {
	Write-Error "Метод '$MethodName' — функция. Для функций доступны только Instead и ModificationAndControl (перехват &Перед/&После к функциям неприменим)."
	exit 1
}

$decoratorRu = $script:decoratorMap[$InterceptorType]

# --- Read existing extension module (if any) ---
$extLines = @()
$extExists = Test-Path $extBsl
if ($extExists) { $extLines = @([System.IO.File]::ReadAllLines($extBsl, [System.Text.Encoding]::UTF8)) }

$existingInterceptors = if ($extExists) { Get-Interceptors $extLines } else { @() }
$existingProcNames = if ($extExists) { Get-ProcNames $extLines } else { @() }

# --- Does the same (method, type) already exist? ---
$dup = $existingInterceptors | Where-Object { $_.Type -eq $decoratorRu -and $_.Method -ieq $MethodName } | Select-Object -First 1

$enc = New-Object System.Text.UTF8Encoding($true)

if ($dup) {
	if ($InterceptorType -ne "ModificationAndControl") {
		Write-Host "[ПРОПУЩЕН] Перехватчик &$decoratorRu(`"$MethodName`") уже есть в модуле — дубль не создаётся."
		Write-Host "     Файл: $extBsl"
		exit 0
	}
	# ---- RESYNC (&ИзменениеИКонтроль) — via Invoke-Resync ----
	$rel = Get-RelPartsUnder $ExtensionPath $extBsl
	$logicalModule = Get-ModulePathFromRel $rel
	$relJoined = ($rel -join '') -replace '.bsl$', ''
	$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\/:*?"<>|]', '_'))
	$folder = Join-Path $runRoot (Join-Path $relJoined $method.Canonical)
	$res = Invoke-Resync $extBsl $extLines $dup $method $logicalModule $folder $enc
	switch ($res.Status) {
		'АКТУАЛЕН' { Write-Host "[АКТУАЛЕН] &ИзменениеИКонтроль(`"$MethodName`") — оригинал не менялся, изменений нет." }
		'АКТУАЛИЗИРОВАН' {
			$msg = "[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль(`"$MethodName`") — тело обновлено, правок сохранено: $($res.Transferred)"
			if ($res.Absorbed -gt 0) { $msg += ", перенесено в основную конфигурацию: $($res.Absorbed)" }
			Write-Host $msg
		}
		'ПЕРЕНЕСЕНО В ОСНОВНУЮ' {
			Write-Host "[ПЕРЕНЕСЕНО В ОСНОВНУЮ] &ИзменениеИКонтроль(`"$MethodName`") — все правки ($($res.Absorbed)) уже в основной конфигурации, перехватчик можно удалить."
		}
		'ЧАСТИЧНО' {
			$idx = Write-ResyncIndex $runRoot @($res) $extName $ConfigPath "АКТУАЛИЗАЦИЯ" $enc
			$msg = "[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль(`"$MethodName`") — сохранено: $($res.Transferred), конфликтов: $($res.Disputed)"
			if ($res.Absorbed -gt 0) { $msg += ", перенесено в основную: $($res.Absorbed)" }
			Write-Host $msg
			Write-Host "     Конфликт помечен // [РЕСИНК-КОНФЛИКТ]. Папка метода: $($res.ConflictDir)"
			if ($idx) { Write-Host "     Индекс: $idx" }
		}
		default { Write-Host "[$($res.Status)] &ИзменениеИКонтроль(`"$MethodName`") — $($res.Reason)" }
	}
	if ($res.AbsorbedNotes) { foreach ($n in $res.AbsorbedNotes) { Write-Host "     ⚠ комментарий не перенесён (код в основной конфигурации): $n" } }
	Write-Host "     Файл: $extBsl"
	exit 0
}

# ============================================================================
# New interceptor: compute name, build block, place (region-aware)
# ============================================================================

# Procedure name with collision handling
$candidate = "${namePrefix}$($method.Canonical)"
$taken = @($existingProcNames | ForEach-Object { $_.ToLower() })
$interceptorName = $candidate
if ($taken -contains $candidate.ToLower()) {
	if ($InterceptorType -eq "ModificationAndControl") {
		$interceptorName = "${candidate}_ИзменениеИКонтроль"
	} else {
		$interceptorName = "${candidate}_$decoratorRu"
	}
}

$core = Build-InterceptorCore $method $InterceptorType $interceptorName

# --- Region-aware placement ---
# Find innermost region in the source chain that already exists in the extension module.
$chain = $method.Chain
$reuseRegionIdx = -1     # index in chain of the region to reuse
$reuseLineIdx = -1       # line in extLines of its #Область
if ($extExists) {
	for ($c = $chain.Count - 1; $c -ge 0; $c--) {
		if ($chain[$c].Kind -eq 'region') {
			$rname = $chain[$c].Name
			for ($li = 0; $li -lt $extLines.Count; $li++) {
				if ($extLines[$li].Trim() -match ('^#Область\s+' + [regex]::Escape($rname) + '\s*$')) {
					$reuseRegionIdx = $c; $reuseLineIdx = $li; break
				}
			}
		}
		if ($reuseRegionIdx -ge 0) { break }
	}
}

if ($reuseRegionIdx -ge 0) {
	# Insert inside the existing region, before its matching #КонецОбласти.
	# Wrappers inner to the reused region are emitted around the method; outer are inherited.
	$innerChain = @()
	for ($c = $reuseRegionIdx + 1; $c -lt $chain.Count; $c++) { $innerChain += $chain[$c] }

	$block = Build-WrappedBlock $innerChain $core

	# find matching #КонецОбласти for the reused region
	$depth = 0; $closeIdx = -1
	for ($li = $reuseLineIdx; $li -lt $extLines.Count; $li++) {
		$t = $extLines[$li].Trim()
		if ($t -match '^#Область\s') { $depth++ }
		elseif ($t -match '^#КонецОбласти') { $depth--; if ($depth -eq 0) { $closeIdx = $li; break } }
	}
	if ($closeIdx -lt 0) { Write-Error "Не найден #КонецОбласти для региона (переиспользование)"; exit 1 }

	# strip trailing blank lines of the region content, then insert with air
	$lastContent = $closeIdx - 1
	while ($lastContent -ge 0 -and $extLines[$lastContent].Trim() -eq "") { $lastContent-- }
	$out = @()
	for ($j = 0; $j -le $lastContent; $j++) { $out += $extLines[$j] }
	$out += ""
	foreach ($l in $block) { $out += $l }
	$out += ""
	for ($j = $closeIdx; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
	[System.IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)
	$placement = "в существующий регион '$($chain[$reuseRegionIdx].Name)'"
} else {
	# Build full wrapper chain (source order) and append (or create file).
	$block = Build-WrappedBlock $chain $core
	$blockText = ($block -join "`r`n") + "`r`n"

	$bslDir = Split-Path $extBsl -Parent
	if (-not (Test-Path $bslDir)) { New-Item -ItemType Directory -Path $bslDir -Force | Out-Null }

	if ($extExists) {
		$existing = [System.IO.File]::ReadAllText($extBsl, $enc)
		if ([string]::IsNullOrWhiteSpace($existing)) {
			# Borrowed-but-empty module (e.g. cfe-borrow form module)
			[System.IO.File]::WriteAllText($extBsl, $blockText, $enc)
			$placement = "заполнен модуль"
		} else {
			$sep = if ($existing.EndsWith("`n")) { "`r`n" } else { "`r`n`r`n" }
			[System.IO.File]::WriteAllText($extBsl, ($existing + $sep + $blockText), $enc)
			$placement = "дописан в модуль"
		}
	} else {
		[System.IO.File]::WriteAllText($extBsl, $blockText, $enc)
		$placement = "создан модуль"
	}
}

# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
if ($flagTarget) {
	Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
}

Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
Write-Host "     Файл:       $extBsl"
Write-Host "     Процедура:  $interceptorName($($method.ParamsText -replace '\s+', ' '))"
if ($method.Context) { Write-Host "     Контекст:   $($method.Context)" }
if ($chain.Count -gt 0) {
	$chainDesc = ($chain | ForEach-Object { if ($_.Kind -eq 'region') { "Область:$($_.Name)" } else { "Если:$($_.Cond)" } }) -join " > "
	Write-Host "     Обрамление: $chainDesc"
}
