"""
Async-First Command Processors for Dr.Egeria v2.
"""
import uuid
import asyncio
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from loguru import logger

from pyegeria import EgeriaTech, PyegeriaException, NO_ELEMENTS_FOUND
from pyegeria.core.utils import make_format_set_name_from_type
from pyegeria.view.base_report_formats import select_report_spec
from pyegeria.view.output_formatter import generate_output, format_for_markdown_table

from md_processing.v2.extraction import DrECommand
from md_processing.v2.parsing import AttributeFirstParser
from md_processing.md_processing_utils.md_processing_constants import get_command_spec
from md_processing.md_processing_utils.common_md_utils import (
    update_element_dictionary, get_element_dictionary, is_present, find_key_with_value
)

class AsyncBaseCommandProcessor(ABC):
    """
    Base class for all v2 Dr.Egeria command processors.
    Handles the standard flow: Parse -> Validate -> Execute/Validate (Dry Run).
    """

    def __init__(self, client: EgeriaTech, command: DrECommand, context: Optional[Dict[str, Any]] = None):
        self.client = client
        self.command = command
        self.context = context or {}
        
        # Ensure a request_id exists
        if "request_id" not in self.context:
            self.context["request_id"] = str(uuid.uuid4())
            
        directive = self.context.get("directive", "process")
        self.parser = AttributeFirstParser(self.command, client=self.client, directive=directive)
        self.parsed_output = None
        self.as_is_element = None
        self.related_results = []
        
        # Resolve canonical name from spec
        spec = self.get_command_spec()
        if spec:
            self.canonical_object_type = spec.get("display_name") or self.command.object_type
            if " " in self.canonical_object_type:
                # Use only the object part if display_name is "Verb Object"
                _verb, obj = spec.get("verb", ""), self.canonical_object_type
                if obj.startswith(f"{_verb} "):
                    self.canonical_object_type = obj[len(_verb)+1:]
        else:
            self.canonical_object_type = self.command.object_type

    def get_command_spec(self) -> Dict[str, Any]:
        """Return the JSON specification for this command family."""
        # Use full command name to ensure alternate names match correctly
        spec = get_command_spec(f"{self.command.verb} {self.command.object_type}")
        return spec or {}

    def add_related_result(self, label: str, guid: Optional[str] = None, status: str = "success", message: Optional[str] = None):
        """Record the outcome of a secondary operation."""
        self.related_results.append({
            "label": label, "guid": guid, "status": status, "message": message
        })

    async def execute(self) -> Dict[str, Any]:
        """
        Orchestrate the command execution flow.
        Returns a dictionary containing the output markdown and execution metadata.
        """
        directive = self.context.get("directive", "process")
        
        # 1. Parse attributes using the spec-agnostic parser
        spec = self.get_command_spec()
        self.parsed_output = self.parser.parse()
        attributes = self.parsed_output.get("attributes", {})

        # Extract Display Name if present
        display_name = attributes.get("Display Name", {}).get("value")
        if not display_name:
            # Fallback to other name-like attributes if possible
            for k, v in attributes.items():
                if any(k.endswith(s) for s in [" Name", " ID", " Id"]) and k != "Qualified Name":
                    display_name = v.get("value")
                    if display_name:
                        break
        if not display_name:
            display_name = attributes.get("Name", {}).get("value")

        if display_name:
            self.parsed_output["display_name"] = display_name

        # 1a. Ensure qualified_name is derived early if possible
        if not self.parsed_output.get("qualified_name"):
            qn = self.derive_qualified_name(attributes)
            if qn:
                self.parsed_output["qualified_name"] = qn
                # Inject into attributes for consistency with legacy prop body helpers
                if "Qualified Name" not in attributes:
                    attributes["Qualified Name"] = {
                        "value": qn,
                        "valid": True,
                        "exists": True,
                        "status": "INFO"
                    }

        # 1b. Handle 'display' directive
        if directive == "display":
            self.as_is_element = await self.fetch_as_is()
            if self.as_is_element:
                output = await self.render_result_markdown(self.as_is_element.get('elementHeader', {}).get('guid'))
            else:
                output = await self.display_only()
            
            return {
                "output": output,
                "status": "success",
                "message": f"Displayed {self.command.verb} {self.command.object_type}",
                "verb": self.command.verb,
                "object_type": self.command.object_type,
                "display_name": self.parsed_output.get("display_name"),
                "qualified_name": self.parsed_output.get("qualified_name"),
                "warnings": self.parsed_output.get("warnings", [])
            }

        # 2. Pre-flight Validation (check required fields, etc.)
        if not self.parsed_output.get("valid", True):
            errors = self.parsed_output.get("errors", [])
            err_msg = "; ".join(errors) if errors else "General validation failure"
            full_message = f"Validation failed: {err_msg}"
            logger.error(f"{full_message} for {self.command.verb} {self.command.object_type}")
            logger.debug(f"Parsed Output: {self.parsed_output}")
            
            # Even if invalid, we want to show the diagnosis table if possible
            analysis = await self.validate_only()
            return {
                "output": analysis if directive == "validate" else self.command.raw_block,
                "analysis": analysis,
                "status": "failure",
                "message": full_message,
                "verb": self.command.verb,
                "object_type": self.command.object_type,
                "display_name": self.parsed_output.get("display_name"),
                "qualified_name": self.parsed_output.get("qualified_name"),
                "found": self.parsed_output.get("exists", False),
                "errors": errors
            }

        # 3. Handle As-Is state and other pre-execution steps
        # Fetch As-Is state (Lookup by GUID or QN)
        # We do this BEFORE recording in planned_elements to avoid self-shadowing 
        # (where an element sees itself in 'planned' and skips the Egeria lookup)
        self.as_is_element = await self.fetch_as_is()
        
        # 4a. Check for duplicate display_name if we are creating a new element
        if not self.as_is_element and self.command.verb in ["Create", "Define", "Register", "Add", "Upsert"]:
            display_name = attributes.get("Display Name", {}).get("value")
            if display_name:
                existing_guid = await self.resolve_element_guid(display_name, tech_type=self.command.object_type)
                if existing_guid and not existing_guid.startswith("(Planned:"):
                    msg = f"Warning: An element with Display Name '{display_name}' already exists in Egeria (QN or Display Name match)."
                    logger.warning(msg)
                    if "warnings" not in self.parsed_output:
                        self.parsed_output["warnings"] = []
                    if msg not in self.parsed_output["warnings"]:
                        self.parsed_output["warnings"].append(msg)

        if self.as_is_element:
            # Transition to Update
            self.command.verb = "Update"
            self.parsed_output["exists"] = True
            header = self.as_is_element.get('elementHeader', {})
            self.parsed_output["guid"] = header.get('guid')

        # 5. Record this element in the shared 'planned_elements' set
        current_qn = self.parsed_output.get("qualified_name")
        planned = self.context.get("planned_elements")
        if isinstance(planned, set) and current_qn and self.command.verb in ["Create", "Define", "Register", "Add", "Update", "Modify", "Upsert"]:
            planned.add(current_qn)

        # 6. Dry-run validation (optional/future)

        # 7. Global Lookups and Existential Checks for References
        # We check all attributes that look like references or are marked as Reference Name/ID in spec
        for attr_name, attr_data in attributes.items():
            # Get the style from the spec if possible
            spec_style = "Simple"
            spec = self.get_command_spec()
            spec_existing = ""
            if spec:
                spec_attrs = spec.get("Attributes", spec.get("attributes", []))
                for a in spec_attrs:
                    if isinstance(a, dict):
                        if a.get("name") == attr_name:
                            spec_style = a.get("style", "Simple")
                            spec_existing = a.get("existing_element", "")
                            break
                        elif attr_name in a:
                            val = a[attr_name]
                            if isinstance(val, dict):
                                spec_style = val.get("style", "Simple")
                                spec_existing = val.get("existing_element", "")
                                break
            
            # Heuristics for identifying what attributes represent references to other elements
            # We exclude common string attributes that might contain keywords like "Reference" or "Name"
            non_ref_names = [
                "Display Name", "Qualified Name", "Description", 
                "Reference Abstract", "Reference Title", "Reference Description",
                "Abstract", "Title", "Category", "Organization", "URL", "License", "Copyright",
                "Identifier", "Domain", "Summary"
            ]
            
            is_ref_candidate = (
                spec_style in ["Reference Name", "ID", "GUID", "Reference", "Reference Name List"]
            ) or (
                spec_existing != "" and spec_style in ["Simple", "Simple List", "List", "NameList"]
            ) or (
                any(k in attr_name for k in ["Id", "GUID", "Name", "Reference"]) 
                and attr_name not in non_ref_names
                and spec_style not in ["Simple", "Enum", "Valid Value", "ValidValue", "Dictionary", "KeyValue", "Enumeration", "Integer", "Boolean"]
            )
            
            # More precise check: if it's already got a GUID or guid_list, don't re-resolve. 
            # If it's a value but no GUID/guid_list, and it's a candidate, try to resolve.
            val = attr_data.get("value")
            if val and not attr_data.get("guid") and not attr_data.get("guid_list") and is_ref_candidate:
                if isinstance(val, list):
                    guid_list = []
                    for item in val:
                        guid = await self.resolve_element_guid(item, tech_type=spec_existing)
                        if guid:
                            guid_list.append(guid)
                    if guid_list:
                        attr_data["guid_list"] = guid_list
                        attr_data["exists"] = len(guid_list) == len(val)
                        if any(g.startswith("(Planned:") for g in guid_list):
                            attr_data["is_planned"] = True
                else:
                    # Try to resolve GUID from cache or Egeria
                    guid = await self.resolve_element_guid(val, tech_type=spec_existing)
                    if guid:
                        attr_data["guid"] = guid
                        # If it's a 'Planned' element, it counts as exists for validation
                        attr_data["exists"] = True
                        if guid.startswith("(Planned:"):
                            attr_data["is_planned"] = True
                    else:
                        # If it's a candidate ref and we couldn't resolve it, mark as not found
                        attr_data["exists"] = False
                        attr_data["valid"] = True # Treat as valid to avoid blocking; let Egeria validate
                        msg = f"Referenced element '{val}' for attribute '{attr_name}' not found."
                        if attr_data.get("warnings") is None: attr_data["warnings"] = []
                        attr_data["warnings"].append(msg)
                        
                        # Always treat as warning, never as error, per user requirement to continue
                        logger.warning(msg)
                        if "warnings" not in self.parsed_output:
                            self.parsed_output["warnings"] = []
                        self.parsed_output.get("warnings", []).append(msg)

        # 7. Check for existence of the target element (As-Is state)
        if self.as_is_element:
            logger.debug(f"Element found! GUID: {self.parsed_output.get('guid')}")
        else:
            logger.debug(f"Element NOT found for QN: '{current_qn}'")
            self.parsed_output["exists"] = False
            if self.command.verb == "Update":
                logger.debug(f"Target element for 'Update' not found: {self.parsed_output.get('qualified_name') or self.command.object_type}")
                if "errors" not in self.parsed_output:
                    self.parsed_output["errors"] = []
                self.parsed_output["errors"].append(f"Target element for 'Update' not found.")
                
                analysis = await self.validate_only()
                return {
                    "output": analysis if directive == "validate" else self.command.raw_block,
                    "analysis": analysis,
                    "status": "failure",
                    "message": f"Target element for Update not found.",
                    "verb": self.command.verb,
                    "object_type": self.command.object_type,
                    "found": False,
                    "errors": self.parsed_output["errors"]
                }

        # 8. Decouple Analysis from Output
        analysis = await self.validate_only()

        # 9. Action Dispatch
        if directive == "validate":
            status = "success" if not self.parsed_output.get("errors") else "failure"
            guid = self.parsed_output.get("guid")
            return {
                "output": analysis,
                "analysis": analysis,
                "status": status,
                "message": f"Validated {self.command.verb} {self.command.object_type}" + (f" (GUID: {guid})" if guid else ""),
                "verb": self.command.verb,
                "object_type": self.command.object_type,
                "display_name": self.parsed_output.get("display_name"),
                "qualified_name": self.parsed_output.get("qualified_name"),
                "guid": self.parsed_output.get("guid"),
                "found": self.parsed_output.get("exists", False),
                "warnings": self.parsed_output.get("warnings", [])
            }
        
        # Check for blockers before applying changes
        if self.parsed_output.get("errors"):
            guid = self.parsed_output.get("guid")
            return {
                "output": self.command.raw_block,
                "analysis": analysis,
                "status": "failure",
                "message": f"Execution blocked: {'; '.join(self.parsed_output['errors'])}" + (f" (GUID: {guid})" if guid else ""),
                "verb": self.command.verb,
                "object_type": self.command.object_type,
                "display_name": self.parsed_output.get("display_name"),
                "qualified_name": self.parsed_output.get("qualified_name"),
                "guid": self.parsed_output.get("guid"),
                "found": self.parsed_output.get("exists", False)
            }

        output = await self.apply_changes()
        
        # 10. Post-execution: Update the cache on success
        guid = self.parsed_output.get("guid") or attributes.get("guid")
        if output and not output.startswith(self.command.raw_block): # Basic success check
            qn = self.parsed_output.get("qualified_name")
            if qn and guid:
                d_name = self.parsed_output.get("display_name") or qn
                update_element_dictionary(qn, {"guid": guid, "display_name": d_name})

        message = f"Executed {self.command.verb} {self.command.object_type}" + (f" (GUID: {guid})" if guid else "")
        if self.related_results:
            rel_parts = [
                f"{r['label']}" + (f" (GUID: {r['guid']})" if r.get('guid') else "") + 
                (f" - {r['status'].upper()}" if r.get('status') != "success" else "")
                for r in self.related_results
            ]
            message += " | Related: " + "; ".join(rel_parts)

        return {
            "output": output,
            "analysis": analysis,
            "status": "success",
            "message": message,
            "verb": self.command.verb,
            "object_type": self.command.object_type,
            "display_name": self.parsed_output.get("display_name"),
            "guid": guid,
            "qualified_name": self.parsed_output.get("qualified_name"),
            "found": self.parsed_output.get("exists", False),
            "warnings": self.parsed_output.get("warnings", [])
        }

    def derive_qualified_name(self, attributes: Optional[Dict[str, Any]] = None) -> str:
        """
        Derive a qualified_name from 'Display Name' (or other basis) and the command spec.
        """
        if attributes is None:
            attributes = self.parsed_output.get("attributes", {})
            
        # 1. Find the best attribute to use as a name basis
        display_name = attributes.get("Display Name", {}).get("value")
        if not display_name:
            # Look for attributes marked as 'is_qualified_name_basis' in the spec
            spec = self.get_command_spec()
            basis_attr = None
            if spec and "attributes" in spec:
                for attr_name, attr_spec in spec["attributes"].items():
                    if attr_spec.get("is_qualified_name_basis"):
                        basis_attr = attr_name
                        break
            
            if basis_attr:
                display_name = attributes.get(basis_attr, {}).get("value")

        if not display_name:
            # Look for ANY attribute ending in ' Name', ' ID', or ' Id' (e.g., 'Glossary Name', 'Term ID')
            for k, v in attributes.items():
                if any(k.endswith(s) for s in [" Name", " ID", " Id"]) and k != "Qualified Name":
                    display_name = v.get("value")
                    if display_name:
                        break
        
        if not display_name:
            # Try 'Name' strictly if present
            display_name = attributes.get("Name", {}).get("value")

        if not display_name:
            # Return empty if no basis found - will fail validation if required
            return ""

        spec = self.get_command_spec()
        qn_prefix = (spec.get("qn_prefix") if spec else None) or self.command.object_type
        
        # Strip trailing colon if present
        if qn_prefix.endswith(':'):
            qn_prefix = qn_prefix[:-1]

        # Extract local qualifier (Namespace Path) and Version Identifier if present 
        local_qualifier = attributes.get("Namespace Path", {}).get("value")
        version_identifier = attributes.get("Version Identifier", {}).get("value")

        # Reach into 'collections' subclient which is guaranteed to have the helper
        helper = getattr(self.client, 'collections', self.client)
        if hasattr(helper, "__create_qualified_name__"):
            return helper.__create_qualified_name__(
                type_name=qn_prefix,
                display_name=display_name,
                local_qualifier=local_qualifier,
                version_identifier=version_identifier or ""
            )
        else:
            # Basic fallback if SDK helper unavailable
            q_name = f"{qn_prefix}::{display_name}"
            if local_qualifier:
                q_name = f"{local_qualifier}::{q_name}"
            if version_identifier:
                q_name = f"{q_name}::{version_identifier}"
            return q_name

    async def render_result_markdown(self, guid: str) -> str:
        """
        Fetch the element by GUID and render it into markdown using the appropriate report_spec.
        """
        if not guid:
            return self.command.raw_block
            
        # 1. Determine the report spec name
        # Convention: <Type>-DrE (spaces replaced with dashes)
        # Use canonical_object_type to ensure we get 'Glossary-Term-DrE' instead of 'Term-DrE'
        base_report_spec_name = make_format_set_name_from_type(self.canonical_object_type)
        
        from md_processing.md_processing_utils.common_md_proc_utils import EGERIA_USAGE_LEVEL
        level_suffix = f"-{EGERIA_USAGE_LEVEL.capitalize()}" if getattr(EGERIA_USAGE_LEVEL, 'capitalize', None) else "-Basic"
        report_spec_name = f"{base_report_spec_name}{level_suffix}"
        
        # 2. Fetch the element dictionary
        try:
            element = await self.fetch_element(guid)
            if not element or isinstance(element, str):
                logger.warning(f"Could not fetch element {guid} for rendering.")
                return self.command.raw_block
        except Exception as e:
            logger.error(f"Error fetching element for rendering: {e}")
            return self.command.raw_block

        # 3. Select the report spec
        columns_struct = select_report_spec(report_spec_name, "MD")
        if not columns_struct:
            columns_struct = select_report_spec(base_report_spec_name, "MD")
            
        if not columns_struct:
            msg = f"Report spec '{report_spec_name}' not found. Falling back to default."
            logger.warning(msg)
            if "warnings" not in self.parsed_output:
                self.parsed_output["warnings"] = []
            if msg not in self.parsed_output["warnings"]:
                self.parsed_output["warnings"].append(msg)
            columns_struct = select_report_spec("Referenceable", "MD")

        # 4. Generate the output
        try:
            markdown = generate_output(
                elements=[element],
                search_string=self.parsed_output.get("qualified_name", "Created/Updated Element"),
                entity_type=self.command.object_type,
                output_format="MD",
                columns_struct=columns_struct
            )
            return markdown
        except Exception as e:
            logger.error(f"Error generating markdown: {e}")
            return self.command.raw_block

    async def fetch_element(self, guid: str) -> Optional[Dict[str, Any]]:
        """
        Fetch the details of an element by GUID. 
        Subclasses should override if MetadataExpert/Explorer is unavailable or if a specific OMAS method is needed.
        """
        try:
            # First try ClassificationExplorer (most standard and lightweight)
            print(f"DEBUG: fetch_element('{guid}') using client {self.client}")
            res = await getattr(self.client, "_async_get_element_by_guid_")(guid)
            print(f"DEBUG: fetch_element returned {res is not None}")
            if res and isinstance(res, dict):
                # The structure from classification-explorer comes under "element" usually
                if "element" in res:
                    return res["element"]
                return res
            return res
        except Exception as e:
            logger.debug(f"ClassificationExplorer fetch failed: {e}")

        try:
            # Fallback to MetadataExpert (more detailed properties)
            # Reordered subclients in EgeriaTech ensure this hits metadata-expert first
            res = await self.client._async_get_metadata_element_by_guid(guid)
            if res and isinstance(res, dict):
                return res
        except Exception as e:
            logger.debug(f"MetadataExpert/Explorer fetch failed: {e}")

        return None

    async def resolve_element_guid(self, name_or_guid: str, tech_type: Optional[str] = None) -> Optional[str]:
        """
        Resolves a name or GUID to a GUID using various strategies.
        Returns None if not found, or f"(Planned: {name})" if it's a forward reference.
        """
        if not name_or_guid or not str(name_or_guid).strip():
            return None
        
        name_or_guid = str(name_or_guid).strip()
        
        # 1. Is it a GUID?
        try:
            uuid.UUID(name_or_guid)
            return name_or_guid
        except ValueError:
            pass
            
        # 2. Check local cache (real elements first!)
        key = find_key_with_value(name_or_guid)
        if key:
            cache_info = get_element_dictionary().get(key)
            if cache_info and "guid" in cache_info:
                return cache_info["guid"]

        # 3. Check current batch (planned_elements)
        planned = self.context.get("planned_elements", set())
        if name_or_guid in planned:
            return f"(Planned: {name_or_guid})"
        
        # 4. Check Egeria (Existence Check)
        try:
            # 4a. Use __async_get_guid__ as suggested by user for existence check
            # This is a lightweight call to classification-explorer
            try:
                # Try as Qualified Name first (with tech_type hint)
                res = await self.client.__async_get_guid__(qualified_name=name_or_guid, tech_type=tech_type)
                if res and res != NO_ELEMENTS_FOUND:
                    logger.debug(f"resolve_element_guid: __async_get_guid__ for QN '{name_or_guid}' (hint={tech_type}) returned: {res}")
                    return res
                
                # Try as Display Name (with tech_type hint)
                res = await self.client.__async_get_guid__(display_name=name_or_guid, tech_type=tech_type)
                if res and res != NO_ELEMENTS_FOUND:
                    logger.debug(f"resolve_element_guid: __async_get_guid__ for Display Name '{name_or_guid}' (hint={tech_type}) returned: {res}")
                    return res

                # 4b. Use SDK's strict name-to-GUID resolution as fallback
                # This checks QN, Display Name, Resource Name, and Identifier.
                try:
                    res = await self.client._async_get_guid_for_name(name_or_guid)
                    # Ensure it's not a "not found" indicator string
                    if res and isinstance(res, str) and not res.startswith("No ") and " found" not in res and not res.startswith("(Planned:"):
                        logger.debug(f"resolve_element_guid: SDK strict lookup for '{name_or_guid}' returned: {res}")
                        return res
                except PyegeriaException as e:
                    # Catch multiple matches error
                    if "Multiple elements found" in str(e):
                        msg = f"Multiple elements found for name '{name_or_guid}'. Please use a unique Qualified Name."
                        logger.error(msg)
                        if "errors" not in self.parsed_output:
                            self.parsed_output["errors"] = []
                        self.parsed_output["errors"].append(msg)
                        return None
                    logger.debug(f"SDK strict lookup failed for '{name_or_guid}': {e}")

            except Exception as e:
                logger.debug(f"__async_get_guid__ failed: {e}")

            # 4c. Try find_method search from spec
            spec = self.get_command_spec()
            find_method_name = spec.get("find_method")
            if find_method_name:
                try:
                    method_parts = find_method_name.split('.')
                    mgr_method = method_parts[-1]
                    method = getattr(self.client, f"_async_{mgr_method}", None)
                    if method:
                        parts = name_or_guid.split("::")
                        basis = parts[-2] if len(parts) >= 4 else parts[-1]
                        results = await method(basis or name_or_guid)
                        if isinstance(results, list) and len(results) > 0:
                            for res in results:
                                qn = (res.get("elementHeader", {}).get("qualifiedName") or 
                                      res.get("properties", {}).get("qualifiedName"))
                                if qn == name_or_guid:
                                    return res.get("guid") or res.get("elementHeader", {}).get("guid")
                except Exception:
                    pass

        except Exception as e:
            logger.debug(f"resolve_element_guid: Unexpected error resolving '{name_or_guid}': {e}")
            
        return None

    async def fetch_as_is(self) -> Optional[Dict[str, Any]]:
        """
        Standardized lookup for the target element. 
        Checks the cache first.
        """
        qn = self.parsed_output.get("qualified_name")
        
        # If QN is missing but Display Name is present, try deriving it
        if not qn:
            qn = self.derive_qualified_name()
            if qn:
                logger.debug(f"fetch_as_is: Derived QN '{qn}' for identification")
                self.parsed_output["qualified_name"] = qn

        if qn:
            # 1. Check Cache
            cache_info = get_element_dictionary().get(qn)
            if cache_info and "guid" in cache_info:
                try:
                    element = await self.fetch_element(cache_info["guid"])
                    if element:
                        return element
                except Exception:
                    pass
            
            # 2. Check Egeria (Harden against missing cache in new sessions)
            guid = await self.resolve_element_guid(qn)
            logger.debug(f"fetch_as_is: resolve_element_guid returned: {guid}")
            if guid and isinstance(guid, str) and not guid.startswith("(Planned:") and not guid.startswith("No "):
                try:
                    # Sanity check: is it a GUID?
                    try:
                        uuid.UUID(guid)
                    except (ValueError, TypeError):
                        logger.debug(f"fetch_as_is: Resolved string '{guid}' is not a valid GUID. Skipping fetch.")
                        return None

                    element = await self.fetch_element(guid)
                    if element:
                        logger.debug(f"fetch_as_is: Element found in Egeria for GUID '{guid}'")
                        # Update cache since we found it
                        attributes = self.parsed_output.get("attributes", {})
                        display_name = attributes.get('Display Name', {}).get('value', qn)
                        update_element_dictionary(qn, {"guid": guid, "display_name": display_name})
                        return element
                    else:
                        logger.debug(f"fetch_as_is: fetch_element returned None for GUID '{guid}'")
                except Exception as e:
                    logger.debug(f"fetch_as_is: fetch_element failed for GUID '{guid}': {e}")
        return None

    @abstractmethod
    async def apply_changes(self) -> str:
        """Apply side-effects to Egeria. Returns the updated markdown."""
        pass

    async def display_only(self) -> str:
        """
        Display-only logic. Skips validation and Egeria lookups.
        Generates a clean markdown summary of the parsed attributes.
        """
        logger.info(f"DISPLAY ONLY: {self.command.verb} {self.command.object_type}")
        if not self.parsed_output:
            logger.error("display_only: self.parsed_output is None!")
            return "### Error: No parsed data available for display."
        
        attributes = self.parsed_output.get("attributes", {})
        qualified_name = self.parsed_output.get("qualified_name")
        
        report = [
            f"### Command: {self.command.verb} {self.command.object_type}",
            f"**Qualified Name**: `{qualified_name}`",
            "",
            "#### Parsed Attributes",
            "| Attribute | Value |",
            "| :--- | :--- |"
        ]
        
        for name, details in attributes.items():
            raw = details.get("value", "")
            val = format_for_markdown_table(raw)
            
            report.append(f"| {name} | {val} |")
            
        report.append("\n---")
        return "\n".join(report)

    async def validate_only(self) -> str:
        """
        Standardized 'dry-run' logic. 
        Generates a rich markdown diagnostic summary of what *would* happen.
        """
        logger.info(f"DRY RUN: Validating {self.command.verb} {self.command.object_type}")
        if not self.parsed_output:
            logger.error("validate_only: self.parsed_output is None!")
            return "### Error: No parsed data available for validation."
        
        attributes = self.parsed_output.get("attributes", {})
        qualified_name = self.parsed_output.get("qualified_name")
        exists = self.parsed_output.get("exists", False)
        errors = self.parsed_output.get("errors", [])
        warnings = self.parsed_output.get("warnings", [])
        
        target_verb = self.command.verb
        guid_info = ""
        if exists:
            guid = self.as_is_element.get('elementHeader', {}).get('guid') or self.parsed_output.get("guid")
            guid_info = f" (GUID: {guid})" if guid else ""
        
        report = [
            f"### Command Analysis: {self.command.verb} {self.command.object_type}",
            f"**Action**: {target_verb}{guid_info}",
            f"**Qualified Name**: `{qualified_name}`",
            "",
            "#### Parsed Attributes",
            "| Attribute | Value | Status |",
            "| :--- | :--- | :--- |"
        ]
        
        for name, details in attributes.items():
            raw = details.get("value", "")
            val = format_for_markdown_table(raw)
            
            # Visual feedback for existential validation
            if details.get("exists") is False:
                status = "❌ Not Found"
            elif details.get("is_planned"):
                status = "🕒 Planned"
            else:
                status = "✅ Valid" if details.get("valid") else "❌ Invalid"
                
            report.append(f"| {name} | {val} | {status} |")
            
        expanded = []
        for name, details in attributes.items():
            raw = details.get("value", "")
            if isinstance(raw, str) and ("\n" in raw or any(raw.strip().startswith(p) for p in ("- ", "* ", "1.", "#", ">", "```"))):
                expanded.append(f"##### {name}\n\n{raw}\n")

        if expanded:
            report.append("\n#### Rendered Attribute Details\n")
            report.extend(expanded)
            
        if errors:
            report.append("\n#### ❌ Errors")
            for err in errors:
                report.append(f"- {err}")
                
        if warnings:
            report.append("\n#### ⚠️ Warnings")
            for warn in warnings:
                report.append(f"- {warn}")
                
        report.append("\n---")
        return "\n".join(report)

    async def sync_members(self, 
                           as_is_guids: set, 
                           to_be_guids: set, 
                           add_coro, 
                           remove_coro,
                           replace_all: bool = True) -> Dict[str, List[str]]:
        """
        Generic async relationship synchronization logic.
        Handles set comparison (As-Is vs To-Be) and executes provided coroutines.
        
        If replace_all is False, it only performs additions.
        """
        to_add = to_be_guids - as_is_guids
        to_remove = (as_is_guids - to_be_guids) if replace_all else set()
        
        results = {"added": [], "removed": [], "errors": []}
        
        if to_add:
            logger.debug(f"Sync: Adding {len(to_add)} members")
            for guid in to_add:
                try:
                    await add_coro(guid)
                    results["added"].append(guid)
                except Exception as e:
                    logger.error(f"Sync: Failed to add member {guid}: {e}")
                    results["errors"].append(f"Add {guid}: {e}")
                
        if to_remove:
            logger.debug(f"Sync: Removing {len(to_remove)} members")
            for guid in to_remove:
                try:
                    await remove_coro(guid)
                    results["removed"].append(guid)
                except Exception as e:
                    logger.error(f"Sync: Failed to remove member {guid}: {e}")
                    results["errors"].append(f"Remove {guid}: {e}")
                
        return results

    def filter_update_properties(self, properties: Dict[str, Any], merge_update: bool) -> Dict[str, Any]:
        """
        Filters properties for an update operation.
        If merge_update is True, it removes all None values to avoid overwriting 
        existing properties with nulls in Egeria.
        """
        if not merge_update:
            return properties
            
        # If merge_update is True, we only keep non-None values.
        # We MUST preserve core identification fields like 'class' and 'typeName'
        # which are used by the SDK to identify the property structure.
        identification_keys = {"class", "typeName"}
        return {k: v for k, v in properties.items() if v is not None or k in identification_keys}
