<?php
namespace Engine\Soap;

trait SoapClientDebugTrait
{
	/**
	 * Returns a low-level snapshot of the SOAP client introspection output.
	 *
	 * Useful for debugging WSDL changes (functions/types) without needing to inspect
	 * the SOAP server manually.
	 */
	public function debug_getIntrospection(): array
	{
		return [
			'class' => get_class($this),
			'__getFunctions' => $this->__getFunctions(),
			'__getTypes' => $this->__getTypes(),
		];
	}

	public function debug_renderIntrospectionMermaid(): string
	{
		$functions = $this->__getFunctions();
		$types = $this->__getTypes();
		if (!is_array($types)) {
			return '';
		}

		$structs = $this->debug_parseSoapStructs($types);

		if (!$structs) {
			return '';
		}

		ksort($structs, SORT_STRING);

		$serviceMethods = [];
		$serviceDeps = [];
		if (is_array($functions)) {
			foreach ($functions as $fn) {
				$parsed = $this->debug_parseSoapFunctionSignature((string)$fn);
				if ($parsed === null) {
					continue;
				}
				[$returnType, $methodName, $params] = $parsed;

				$paramParts = [];
				foreach ($params as $p) {
					$paramParts[] = trim($p['type'] . ' ' . $p['name']);
					if (isset($structs[$p['type']])) {
						$serviceDeps[$p['type']] = true;
					}
				}
				if (isset($structs[$returnType])) {
					$serviceDeps[$returnType] = true;
				}

				$serviceMethods[] = sprintf('+%s %s(%s)', $returnType, $methodName, implode(', ', $paramParts));
			}
		}

		sort($serviceMethods, SORT_STRING);

		$lines = [];
		$lines[] = 'classDiagram';
		$lines[] = 'direction LR';
		if ($serviceMethods) {
			$lines[] = 'class SoapService {';
			foreach ($serviceMethods as $m) {
				$lines[] = '  ' . $m;
			}
			$lines[] = '}';
		}
		foreach ($structs as $name => $fields) {
			$lines[] = "class {$name} {";
			foreach ($fields as $f) {
				$lines[] = "  +{$f['type']} {$f['name']}";
			}
			$lines[] = '}';
		}

		$relations = [];
		if ($serviceDeps) {
			$deps = array_keys($serviceDeps);
			sort($deps, SORT_STRING);
			foreach ($deps as $dep) {
				$relations["SoapService --> {$dep}"] = true;
			}
		}
		foreach ($structs as $name => $fields) {
			foreach ($fields as $f) {
				$target = $f['type'];
				if (isset($structs[$target])) {
					$relations["{$name} --> {$target}"] = true;
				}
			}
		}
		$relLines = array_keys($relations);
		sort($relLines, SORT_STRING);
		foreach ($relLines as $rel) {
			$lines[] = $rel;
		}

		return implode("\n", $lines) . "\n";
	}

	public function debug_renderIntrospectionHtml(): string
	{
		$functions = $this->__getFunctions();
		$types = $this->__getTypes();
		$functionsText = '';
		$typesText = '';
		if (is_array($functions)) {
			foreach ($functions as $fn) {
				$line = $this->debug_unescapeSoapText((string)$fn);
				$functionsText .= $line . "\n";
			}
		}
		else {
			$functionsText = var_export($functions, true);
		}

		if (is_array($types)) {
			foreach ($types as $type) {
				$block = $this->debug_unescapeSoapText((string)$type);
				$typesText .= rtrim($block) . "\n\n";
			}
			$typesText = rtrim($typesText) . "\n";
		}
		else {
			$typesText = var_export($types, true);
		}

		$wsdl = $this->wsdl;
		if ($wsdl === null || $wsdl === '') {
			$wsdl = '(no WSDL)';
		}

		$title = sprintf('SOAP Introspection: %s', $wsdl);
		$titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
		$classEsc = htmlspecialchars(get_class($this), ENT_QUOTES, 'UTF-8');
		$functionsCount = is_array($functions) ? count($functions) : 0;
		$typesCount = is_array($types) ? count($types) : 0;

		$functionsEsc = htmlspecialchars($functionsText, ENT_QUOTES, 'UTF-8');
		$typesEsc = htmlspecialchars($typesText, ENT_QUOTES, 'UTF-8');
		$mermaidText = $this->debug_renderIntrospectionMermaid();
		$mermaidEsc = htmlspecialchars($mermaidText, ENT_QUOTES, 'UTF-8');
		$mermaidId = uniqid('tinaSoapMermaid', false);
		$functionsCount = (int)$functionsCount;
		$typesCount = (int)$typesCount;

		return <<<HTML
<div class="tina-soap-intro">
	<style>
		.tina-soap-intro{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.35;color:#111;}
		.tina-soap-intro .card{border:1px solid #d0d7de;border-radius:10px;background:#fff;box-shadow:0 1px 2px rgba(16,24,40,.06);overflow:hidden;}
		.tina-soap-intro .hdr{padding:14px 16px;border-bottom:1px solid #d0d7de;background:linear-gradient(#f6f8fa,#fff);}
		.tina-soap-intro .hdr h1{font-size:14px;margin:0 0 6px 0;font-weight:600;}
		.tina-soap-intro .meta{font-size:12px;color:#57606a;}
		.tina-soap-intro .pill{display:inline-block;font-size:12px;padding:2px 8px;border-radius:999px;background:#f6f8fa;border:1px solid #d0d7de;margin-right:6px;color:#24292f;}
		.tina-soap-intro .body{padding:12px 16px;}
		.tina-soap-intro .section{border:1px solid #d0d7de;border-radius:10px;background:#fff;margin-top:12px;overflow:hidden;}
		.tina-soap-intro .section-title{padding:10px 12px;background:#f6f8fa;border-bottom:1px solid #d0d7de;font-weight:600;font-size:13px;margin:0;}
		.tina-soap-intro .block{margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:12px;white-space:pre;overflow:auto;max-height:70vh;background:#0b1020;color:#e6edf3;}
		.tina-soap-intro details.section > summary{cursor:pointer;list-style:none;}
		.tina-soap-intro details.section > summary::-webkit-details-marker{display:none;}
		.tina-soap-intro .textarea{width:100%;box-sizing:border-box;border:0;outline:none;resize:vertical;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:12px;white-space:pre;overflow:auto;max-height:70vh;min-height:200px;background:#0b1020;color:#e6edf3;}
	</style>
	<div class="card">
		<div class="hdr">
			<h1>{$titleEsc}</h1>
			<div class="meta">
				<span class="pill">functions: {$functionsCount}</span>
				<span class="pill">types: {$typesCount}</span>
			</div>
			<div class="meta">client class: <code>{$classEsc}</code></div>
		</div>
		<div class="body">
			<div class="section">
				<div class="section-title">Functions</div>
				<pre class="block">{$functionsEsc}</pre>
			</div>
			<div class="section">
				<div class="section-title">Types</div>
				<pre class="block">{$typesEsc}</pre>
			</div>
			<details class="section">
				<summary class="section-title" onclick="var el=document.getElementById('{$mermaidId}'); if(el){el.focus(); el.select();}">Mermaid (classDiagram)</summary>
				<textarea class="textarea" id="{$mermaidId}" readonly onclick="this.focus(); this.select();">{$mermaidEsc}</textarea>
			</details>
		</div>
	</div>
</div>
HTML;
	}

	/**
	 * SOAP's `__getFunctions()` / `__getTypes()` may return strings that contain literal
	 * "\\n" / "\\t" sequences. Convert those into real characters so the output is readable.
	 */
	private function debug_unescapeSoapText(string $text): string
	{
		return str_replace(["\\r\\n", "\\n", "\\t"], ["\n", "\n", "\t"], $text);
	}

	/**
	 * Parses `__getTypes()` entries (which look like `struct Foo { ... }`) into:
	 *
	 * - structName => list of fields (`type` + `name`)
	 *
	 * @return array<string, array<int, array{type:string,name:string}>>
	 */
	private function debug_parseSoapStructs(array $types): array
	{
		$structs = [];
		foreach ($types as $type) {
			$block = trim($this->debug_unescapeSoapText((string)$type));
			if ($block === '') {
				continue;
			}

			if (!preg_match('/^struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{(.*)\}\s*$/s', $block, $m)) {
				continue;
			}

			$name = $m[1];
			$body = trim($m[2]);
			$fields = [];
			foreach (preg_split('/\n+/', $body) as $line) {
				$line = trim($line);
				if ($line === '' || $line === '}') {
					continue;
				}
				$line = rtrim($line, ';');
				if (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)$/', $line, $fm)) {
					$fields[] = ['type' => $fm[1], 'name' => $fm[2]];
				}
			}

			$structs[$name] = $fields;
		}

		return $structs;
	}

	/**
	 * Parses a `__getFunctions()` signature string into return type, method name and params.
	 *
	 * @return array{0:string,1:string,2:array<int,array{type:string,name:string}>}|null
	 */
	private function debug_parseSoapFunctionSignature(string $signature): ?array
	{
		$sig = trim($signature);
		if ($sig === '') {
			return null;
		}

		if (!preg_match('/^(\S+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*$/', $sig, $fm)) {
			return null;
		}

		$returnType = $fm[1];
		$methodName = $fm[2];
		$paramsRaw = trim($fm[3]);
		$params = [];
		if ($paramsRaw !== '') {
			foreach (explode(',', $paramsRaw) as $p) {
				$p = trim($p);
				if ($p === '') {
					continue;
				}
				if (preg_match('/^(\S+)\s+\$?([A-Za-z_][A-Za-z0-9_]*)$/', $p, $pm)) {
					$params[] = ['type' => $pm[1], 'name' => $pm[2]];
				}
				else {
					$params[] = ['type' => $p, 'name' => ''];
				}
			}
		}

		return [$returnType, $methodName, $params];
	}

	public function debug_downloadwsdlAndXSD(string $targetDirectory, int $maxFiles = 500): array
	{
		$wsdlUrl = (string)$this->wsdl;
		$targetDirectory = rtrim($targetDirectory, '/');

		$downloaded = [];
		$queue = [];
		$seen = [];
		$queue[] = ['url' => $wsdlUrl, 'kind' => 'wsdl'];

		$context = $this->debug_buildStreamContext();

		$queueIndex = 0;
		while ($queueIndex < count($queue)) {
			if (count($downloaded) >= $maxFiles) {
				break;
			}

			$item = $queue[$queueIndex++];
			$url = (string)($item['url'] ?? '');
			$kind = (string)($item['kind'] ?? '');
			if ($url === '') {
				continue;
			}

			$urlKey = strtolower($url);
			if (isset($seen[$urlKey])) {
				continue;
			}
			$seen[$urlKey] = true;

			$content = @file_get_contents($url, false, $context);
			if ($content === false) {
				$lastErr = error_get_last();
				$downloaded[] = [
					'url' => $url,
					'kind' => $kind,
					'path' => null,
					'error' => 'failed_to_download',
					'phpError' => ($lastErr['message'] ?? null),
				];
				continue;
			}

			$path = $this->debug_buildDownloadPath($targetDirectory, $url, $kind);
			$bytes = @file_put_contents($path, $content);
			if ($bytes === false) {
				$lastErr = error_get_last();
				$downloaded[] = [
					'url' => $url,
					'kind' => $kind,
					'path' => $path,
					'error' => 'failed_to_write',
					'phpError' => ($lastErr['message'] ?? null),
				];
				continue;
			}
			$downloaded[] = [
				'url' => $url,
				'kind' => $kind,
				'path' => $path,
				'error' => null,
				'phpError' => null,
			];

			$locations = $this->debug_extractSchemaLocations($content);
			if (!$locations) {
				continue;
			}

			$base = $this->debug_baseUrl($url);
			foreach ($locations as $loc) {
				$abs = $this->debug_resolveUrl($base, $loc);
				if ($abs === null) {
					continue;
				}
				$queue[] = ['url' => $abs, 'kind' => 'xsd'];
			}
		}

		return $downloaded;
	}

	private function debug_buildStreamContext()
	{
		$options = $this->options ? $this->options->getOptions() : [];
		$streamContext = [];
		$streamContext['ssl'] = [];

		if (array_key_exists('verify_peer', $options) && $options['verify_peer'] !== NULL) {
			if ($options['verify_peer'] === false) {
				$streamContext['ssl']['verify_peer'] = false;
				$streamContext['ssl']['verify_peer_name'] = false;
				$streamContext['ssl']['allow_self_signed'] = true;
			}
		}

		if (array_key_exists('localCertificate', $options) && $options['localCertificate'] !== NULL) {
			$streamContext['ssl']['local_cert'] = $options['localCertificate']['certificatePath'] ?? null;
			$streamContext['ssl']['passphrase'] = $options['localCertificate']['certificatePassword'] ?? null;
			$streamContext['ssl']['local_pk'] = $options['localCertificate']['privateKeyPath'] ?? null;
		}

		if (!empty($options['HTTPHeaders'])) {
			$HTTPHeaders = [];
			foreach ($options['HTTPHeaders'] as $HTTPHeader) {
				$HTTPHeaders[] = sprintf('%s: %s', $HTTPHeader['key'], str_replace("\n", '', $HTTPHeader['value']));
			}
			$streamContext['http'] = [
				'header' => $HTTPHeaders,
			];
		}

		if (isset($options['connection_timeout']) && $options['connection_timeout'] !== NULL) {
			$streamContext['http']['timeout'] = (int)$options['connection_timeout'];
		}

		return stream_context_create($streamContext);
	}

	private function debug_extractSchemaLocations(string $xml): array
	{
		$locations = [];
		$dom = new \DOMDocument();
		$prev = libxml_use_internal_errors(true);
		$ok = $dom->loadXML($xml);
		libxml_clear_errors();
		libxml_use_internal_errors($prev);
		if (!$ok) {
			return [];
		}

		$xpath = new \DOMXPath($dom);
		$nodes = $xpath->query('//@schemaLocation');
		if ($nodes) {
			foreach ($nodes as $n) {
				$val = trim((string)$n->nodeValue);
				if ($val !== '') {
					$locations[] = $val;
				}
			}
		}

		$nodes = $xpath->query('//@location');
		if ($nodes) {
			foreach ($nodes as $n) {
				$val = trim((string)$n->nodeValue);
				if ($val !== '') {
					$locations[] = $val;
				}
			}
		}

		$clean = [];
		foreach ($locations as $loc) {
			$loc = trim($loc);
			if ($loc === '') {
				continue;
			}
			$clean[$loc] = true;
		}
		return array_keys($clean);
	}

	private function debug_baseUrl(string $url): string
	{
		$p = parse_url($url);
		if (!$p || empty($p['scheme']) || empty($p['host'])) {
			return $url;
		}
		$port = isset($p['port']) ? ':' . $p['port'] : '';
		$path = $p['path'] ?? '';
		$dir = preg_replace('#/[^/]*$#', '/', $path);
		return $p['scheme'] . '://' . $p['host'] . $port . $dir;
	}

	private function debug_resolveUrl(string $base, string $ref): ?string
	{
		$ref = trim($ref);
		if ($ref === '') {
			return null;
		}
		if (preg_match('#^https?://#i', $ref)) {
			return $ref;
		}
		if (str_starts_with($ref, '//')) {
			$bp = parse_url($base);
			if (!$bp || empty($bp['scheme'])) {
				return null;
			}
			return $bp['scheme'] . ':' . $ref;
		}
		if (str_starts_with($ref, '/')) {
			$bp = parse_url($base);
			if (!$bp || empty($bp['scheme']) || empty($bp['host'])) {
				return null;
			}
			$port = isset($bp['port']) ? ':' . $bp['port'] : '';
			return $bp['scheme'] . '://' . $bp['host'] . $port . $ref;
		}
		return $base . $ref;
	}

	private function debug_buildDownloadPath(string $targetDirectory, string $url, string $kind): string
	{
		$p = parse_url($url);
		$host = $p['host'] ?? 'unknown-host';
		$path = $p['path'] ?? '';
		$base = basename($path);
		$query = $p['query'] ?? '';

		$filename = $host.$path;
		if ($base) {
			$filename = $host.$base;
		}
		if ($query) {
			$filename = $host.$query;
		}
		$filename = preg_replace("/(wsdl|xsd)/", '', $filename);
		$filename = preg_replace('/[^A-Za-z0-9_-]+/', '_', $filename);
		$filename = preg_replace('/[_]+/', '_', $filename);
		$suffixParts = [];
		if ($query !== '') {
			$suffixParts[] = sha1($query);
		}
		$suffixParts[] = sha1($url);

		$path = sprintf('%s/%s %s.%s', $targetDirectory, $filename, substr(sha1(implode('', $suffixParts)), 0, 12), $kind);
		return $path;
	}
}
