#!/usr/bin/env python3
"""
OCI Architecture Diagram Generator — Hybrid drawpyo + Custom Icon Injection
Produces .drawio files with Oracle official container styles and OCI Library icons.

HYBRID APPROACH:
- drawpyo handles: File, Page, Object (containers/groups), Edge (connections), save
- Custom icon extractor (kb/diagram/oci-icons.json) handles: multi-cell OCI stencil icons
  injected as raw XML into drawpyo's output

Styles extracted from:
- OCI Library.xml — 224 official OCI service icons
- OCI Architecture Diagram Toolkit v24.2.drawio
- Reference architectures: select-ai-apex, exadb-dr-on-db-at-azure

Usage:
    python3.12 oci_diagram_gen.py --spec architecture-spec.yaml --output diagram.drawio

Or import and use programmatically:
    from oci_diagram_gen import OCIDiagramGenerator
    gen = OCIDiagramGenerator()
    gen.add_region("region1", "US East (Ashburn)", parent="tenancy", ...)
    gen.save("output.drawio")
"""

import json
import os
import re
import yaml
import argparse
import sys
import xml.etree.ElementTree as ET
from typing import Optional

import drawpyo
import drawpyo.diagram

# ============================================================
# OCI OFFICIAL STYLES (from OCI Style Guide for Draw.io v24.2)
# ============================================================
#
# Color palette (no #FFFFFF — use #FCFBFA near-white instead):
#   Charcoal  #312D2A   — text, connectors
#   Warm Gray #9E9892   — container borders
#   Light     #F5F4F2   — region/subnet fill
#   Medium    #E4E1DD   — AD fill
#   Near-White#FCFBFA   — background elements
#   Teal      #2D5967   — infrastructure services
#   Copper    #AA643B   — database services (NOT #AE562C)
#   Burnt-Org #AE562C   — VCN/subnet borders ONLY
#   Purple    #804998   — integration services
#   Muted     #70665E   — legacy, external actors

STYLES = {
    # --- Container styles (OCI Architecture Diagram Toolkit v24.2) ---
    "tenancy": (
        "whiteSpace=wrap;html=1;strokeWidth=1;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#312D2A;strokeColor=#9E9892;fontSize=13;fontStyle=1;"
        "spacingLeft=10;spacingTop=8;rounded=1;arcSize=1;"
    ),
    "region": (
        "whiteSpace=wrap;html=1;align=left;fontFamily=Oracle Sans;"
        "verticalAlign=top;fillColor=#F5F4F2;rounded=1;arcSize=5;"
        "strokeColor=#9E9892;fontColor=#312D2A;fontSize=13;fontStyle=1;"
        "spacingLeft=10;spacingTop=8;"
    ),
    "ad": (
        "whiteSpace=wrap;html=1;strokeWidth=1;align=center;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=#DFDCD8;"
        "fontColor=#312D2A;strokeColor=#9E9892;rounded=1;arcSize=1;"
        "fontSize=12;fontStyle=1;"
    ),
    "fault_domain": (
        "whiteSpace=wrap;html=1;strokeWidth=1;align=center;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=#FCFBFA;"
        "fontColor=#312D2A;strokeColor=#9E9892;rounded=1;arcSize=3;"
        "fontSize=11;"
    ),
    "vcn": (
        "whiteSpace=wrap;html=1;strokeWidth=2;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#AE562C;strokeColor=#AE562C;perimeterSpacing=0;"
        "fontSize=12;fontStyle=1;spacingLeft=8;spacingTop=6;"
    ),
    "subnet": (
        "whiteSpace=wrap;html=1;strokeWidth=1;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#AE562C;strokeColor=#AE562C;fontSize=11;fontStyle=1;"
        "spacingLeft=8;spacingTop=4;"
    ),
    "compartment": (
        "whiteSpace=wrap;html=1;strokeWidth=1;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#312D2A;strokeColor=#9E9892;fontSize=12;fontStyle=1;"
        "spacingLeft=5;spacingTop=5;rounded=1;arcSize=1;"
    ),
    # --- External cloud containers (AWS, Azure, GCP) ---
    # Generic / AWS: dashed muted border (Oracle style for non-Azure clouds)
    "cloud_container": (
        "whiteSpace=wrap;html=1;strokeWidth=2;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#70665E;strokeColor=#70665E;fontSize=12;spacingLeft=5;"
        "spacingTop=5;rounded=1;arcSize=6;"
    ),
    # Azure: solid blue border + light blue fill (matches Oracle ref arch style)
    # Source: confirmed from 4 Oracle reference architecture diagrams (exadb-dr, adb-azure, etc.)
    "azure_container": (
        "whiteSpace=wrap;html=1;strokeWidth=2;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=#EBF5FF;"
        "fontColor=#0050A2;strokeColor=#0078D4;fontSize=12;fontStyle=1;"
        "spacingLeft=5;spacingTop=5;rounded=1;arcSize=3;"
    ),
    # GCP: solid green border + light green fill
    "gcp_container": (
        "whiteSpace=wrap;html=1;strokeWidth=2;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=#E8F5E9;"
        "fontColor=#1E6B3C;strokeColor=#34A853;fontSize=12;fontStyle=1;"
        "spacingLeft=5;spacingTop=5;rounded=1;arcSize=3;"
    ),
    # Oracle Services Network: dashed gray column (right-side managed services panel)
    # Source: confirmed in hub-spoke, OKE, lakehouse Oracle reference architectures
    "oracle_services_network": (
        "whiteSpace=wrap;html=1;strokeWidth=1;dashed=1;dashPattern=6 4;align=left;"
        "fontFamily=Oracle Sans;verticalAlign=top;fillColor=none;"
        "fontColor=#312D2A;strokeColor=#9E9892;fontSize=12;fontStyle=1;"
        "spacingLeft=5;spacingTop=5;rounded=1;arcSize=1;"
    ),

    # --- Service block styles (fallback when no icon available) ---
    # OUTLINED style — Oracle reference archs never use solid-filled rectangles.
    # Fallback uses near-white fill + colored border + colored text to approximate
    # the lightweight look of OCI line-art stencil icons.
    "svc_infra": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#FCFBFA;"
        "strokeColor=#2D5967;strokeWidth=1.5;fontColor=#2D5967;fontSize=11;"
        "fontFamily=Oracle Sans;arcSize=8;verticalAlign=middle;align=center;"
    ),
    "svc_database": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#FCFBFA;"
        "strokeColor=#AA643B;strokeWidth=1.5;fontColor=#AA643B;fontSize=11;"
        "fontFamily=Oracle Sans;arcSize=8;verticalAlign=middle;align=center;"
    ),
    "svc_integration": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#FCFBFA;"
        "strokeColor=#804998;strokeWidth=1.5;fontColor=#804998;fontSize=11;"
        "fontFamily=Oracle Sans;arcSize=8;verticalAlign=middle;align=center;"
    ),
    "svc_dormant": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#FCFBFA;"
        "strokeColor=#9E9892;strokeWidth=1;fontColor=#70665E;fontSize=11;"
        "fontFamily=Oracle Sans;arcSize=8;fontStyle=2;verticalAlign=middle;align=center;"
    ),
    "svc_legacy": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#FCFBFA;"
        "strokeColor=#70665E;strokeWidth=1.5;fontColor=#70665E;fontSize=11;"
        "fontFamily=Oracle Sans;arcSize=8;verticalAlign=middle;align=center;"
    ),
    "obs_bar": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#2D5967;"
        "strokeColor=none;fontColor=#FCFBFA;fontSize=10;"
        "fontFamily=Oracle Sans;arcSize=10;"
    ),

    # --- External actor: person silhouette (head circle + body arc) ---
    # Rendered as raw SVG in to_xml() post-processing, not as a drawpyo stencil.
    # This style is for the invisible connectable group + label below the icon.
    "external_user": (
        "group;fillColor=none;strokeColor=none;connectable=1;pointerEvents=1;"
    ),
    # --- External actor: cloud (internet) ---
    "external_internet": (
        "shape=mxgraph.cisco.clouds.cloud;"
        "sketch=0;fillColor=#2D5967;strokeColor=none;"
        "fontColor=#312D2A;fontSize=12;fontFamily=Oracle Sans;"
        "verticalLabelPosition=bottom;verticalAlign=top;"
        "align=center;html=1;"
    ),
    # --- External actor: generic box ---
    "external_generic": (
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#70665E;"
        "strokeColor=none;fontColor=#FCFBFA;fontSize=12;"
        "fontFamily=Oracle Sans;arcSize=12;verticalAlign=middle;align=center;"
    ),

    # --- Flow number badge (gray circle with white number) ---
    "flow_badge": (
        "ellipse;whiteSpace=wrap;html=1;fillColor=#2D5967;"
        "strokeColor=none;fontColor=#FCFBFA;fontSize=10;"
        "fontFamily=Oracle Sans;fontStyle=1;"
        "verticalAlign=middle;align=center;"
    ),

    # --- Title ---
    "title": (
        "text;html=1;fontSize=18;fontColor=#312D2A;"
        "fontFamily=Oracle Sans;align=center;verticalAlign=middle;fontStyle=1;"
    ),
}

# Connection style strings — used ONLY for raw XML fallback summary stats.
# Actual edge styling is done via drawpyo Edge attributes.
CONN_STYLES = {
    "conn_standard": (
        "endArrow=open;endFill=0;startArrow=none;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
    "conn_db": (
        "endArrow=open;endFill=0;startArrow=none;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
    "conn_adg": (
        "endArrow=open;endFill=0;startArrow=none;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "dashed=1;dashPattern=8 4;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
    "conn_fastconnect": (
        "endArrow=open;endFill=0;startArrow=open;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
    "conn_migration": (
        "endArrow=open;endFill=0;startArrow=none;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "dashed=1;dashPattern=8 4;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
    "conn_etl": (
        "endArrow=open;endFill=0;startArrow=none;startFill=0;html=1;"
        "strokeColor=#312D2A;strokeWidth=1;rounded=1;"
        "edgeStyle=orthogonalEdgeStyle;endSize=6;elbow=vertical;"
        "dashed=1;dashPattern=6 4;"
        "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        "labelBackgroundColor=none;"
    ),
}

# Map service types to style categories
SVC_CATEGORY = {
    # Infrastructure (teal #2D5967)
    "compute": "svc_infra", "vm": "svc_infra", "bare_metal": "svc_infra",
    "load_balancer": "svc_infra", "flexible_lb": "svc_infra",
    "network_lb": "svc_infra",
    "igw": "svc_infra", "internet_gateway": "svc_infra",
    "nat_gateway": "svc_infra", "natgw": "svc_infra",
    "service_gateway": "svc_infra", "sgw": "svc_infra",
    "waf": "svc_infra", "bastion": "svc_infra",
    "network_firewall": "svc_infra",
    "functions": "svc_infra", "oke": "svc_infra",
    "api_gateway": "svc_infra", "apigw": "svc_infra",
    "monitoring": "svc_infra", "logging": "svc_infra",
    "db_management": "svc_infra", "ops_insights": "svc_infra",
    "notifications": "svc_infra", "events": "svc_infra",
    "cloud_guard": "svc_infra",
    "security_zone": "svc_infra",
    "vulnerability_scanning": "svc_infra",
    "object_storage": "svc_infra", "block_storage": "svc_infra",
    "file_storage": "svc_infra",
    "dns": "svc_infra", "traffic_management": "svc_infra",
    "certificates": "svc_infra", "apm": "svc_infra",
    "logging_analytics": "svc_infra",
    "os_management": "svc_infra",
    "resource_manager": "svc_infra",
    "devops": "svc_infra",
    # AI Services (teal — OCI AI service category)
    "gen_ai": "svc_infra", "generative_ai": "svc_infra",
    "language_ai": "svc_infra", "vision_ai": "svc_infra",
    "speech_ai": "svc_infra", "document_understanding": "svc_infra",
    "anomaly_detection": "svc_infra", "digital_assistant": "svc_infra",

    # Database (copper #AA643B)
    "adb": "svc_database", "adb_s": "svc_database",
    "adb_d": "svc_database", "autonomous_db": "svc_database",
    "atp": "svc_database", "adw": "svc_database",
    "dbcs": "svc_database", "db_system": "svc_database",
    "exadata": "svc_database", "exacs": "svc_database",
    "nosql": "svc_database", "mysql": "svc_database",
    "mysql_heatwave": "svc_database",
    "postgresql": "svc_database", "opensearch": "svc_database",
    "cache": "svc_database", "redis": "svc_database",
    "goldengate": "svc_database",
    "data_catalog": "svc_database",
    # Security services that touch data tier use database copper
    "data_safe": "svc_database",
    "vault": "svc_database",

    # Integration (purple #804998)
    "drg": "svc_integration",
    "dynamic_routing_gateway": "svc_integration",
    "streaming": "svc_integration", "kafka": "svc_integration",
    "queue": "svc_integration", "oci_queue": "svc_integration",
    "oic": "svc_integration", "integration_cloud": "svc_integration",
    "fastconnect": "svc_integration",
    "service_connector_hub": "svc_integration",
    "vpn": "svc_integration",

    # Special
    "dormant": "svc_dormant",
    "standby": "svc_dormant",
    "legacy": "svc_legacy",
    "external": "svc_legacy",
}

# ============================================================
# AWS / GCP / AZURE ICON STYLES (mxgraph stencil libraries)
# ============================================================
# draw.io includes official cloud provider shape libraries:
#   AWS:   mxgraph.aws4.*  (orange #ED7100, dark text #232F3E)
#   GCP:   mxgraph.gcp2.*  (blue #4285F4)
#   Azure: mxgraph.azure.* (blue #0078D4)
#
# These are used when a service inside a cloud container specifies
# a cloud_icon field (e.g., cloud_icon: "aws4.ec2").

# AWS service type → (resIcon stencil path, fillColor)
AWS_ICONS = {
    "ec2":           ("mxgraph.aws4.ec2", "#ED7100"),
    "eks":           ("mxgraph.aws4.eks", "#ED7100"),
    "ecs":           ("mxgraph.aws4.ecs", "#ED7100"),
    "lambda":        ("mxgraph.aws4.lambda_function", "#ED7100"),
    "rds":           ("mxgraph.aws4.rds", "#C925D1"),
    "aurora":        ("mxgraph.aws4.aurora", "#C925D1"),
    "dynamodb":      ("mxgraph.aws4.dynamodb", "#C925D1"),
    "s3":            ("mxgraph.aws4.s3", "#3F8624"),
    "elb":           ("mxgraph.aws4.elb", "#8C4FFF"),
    "alb":           ("mxgraph.aws4.application_load_balancer", "#8C4FFF"),
    "cloudfront":    ("mxgraph.aws4.cloudfront", "#8C4FFF"),
    "vpc":           ("mxgraph.aws4.vpc", "#8C4FFF"),
    "direct_connect":("mxgraph.aws4.direct_connect", "#8C4FFF"),
    "route53":       ("mxgraph.aws4.route_53", "#8C4FFF"),
    "sqs":           ("mxgraph.aws4.sqs", "#E7157B"),
    "sns":           ("mxgraph.aws4.sns", "#E7157B"),
    "kinesis":       ("mxgraph.aws4.kinesis", "#8C4FFF"),
    "api_gateway":   ("mxgraph.aws4.api_gateway", "#E7157B"),
    "cognito":       ("mxgraph.aws4.cognito", "#DD344C"),
    "iam":           ("mxgraph.aws4.iam", "#DD344C"),
    "cloudwatch":    ("mxgraph.aws4.cloudwatch_2", "#E7157B"),
    "sagemaker":     ("mxgraph.aws4.sagemaker", "#01A88D"),
    "bedrock":       ("mxgraph.aws4.bedrock", "#01A88D"),
}

# GCP service type → (shape path, fillColor)
GCP_ICONS = {
    "gce":           ("mxgraph.gcp2.compute_engine", "#4285F4"),
    "gke":           ("mxgraph.gcp2.google_kubernetes_engine", "#4285F4"),
    "cloud_sql":     ("mxgraph.gcp2.cloud_sql", "#4285F4"),
    "bigquery":      ("mxgraph.gcp2.bigquery", "#4285F4"),
    "cloud_storage": ("mxgraph.gcp2.cloud_storage", "#4285F4"),
    "cloud_run":     ("mxgraph.gcp2.cloud_run", "#4285F4"),
    "cloud_functions":("mxgraph.gcp2.cloud_functions", "#4285F4"),
    "pub_sub":       ("mxgraph.gcp2.cloud_pubsub", "#4285F4"),
}

def _aws_icon_style(service_type: str) -> Optional[str]:
    """Return draw.io style string for an AWS service icon, or None."""
    entry = AWS_ICONS.get(service_type)
    if not entry:
        return None
    stencil, fill = entry
    return (
        f"outlineConnect=0;fontColor=#232F3E;gradientColor=none;"
        f"fillColor={fill};strokeColor=none;dashed=0;"
        f"verticalLabelPosition=bottom;verticalAlign=top;"
        f"align=center;html=1;fontSize=10;fontStyle=0;"
        f"aspect=fixed;"
        f"shape=mxgraph.aws4.resourceIcon;resIcon={stencil};"
    )

def _gcp_icon_style(service_type: str) -> Optional[str]:
    """Return draw.io style string for a GCP service icon, or None."""
    entry = GCP_ICONS.get(service_type)
    if not entry:
        return None
    stencil, fill = entry
    return (
        f"outlineConnect=0;fontColor=#232F3E;gradientColor=none;"
        f"fillColor={fill};strokeColor=none;dashed=0;"
        f"verticalLabelPosition=bottom;verticalAlign=top;"
        f"align=center;html=1;fontSize=10;fontStyle=0;"
        f"aspect=fixed;"
        f"shape={stencil};"
    )


# Map observability bar labels to icon service types
OBS_TYPE_MAP = {
    "monitoring": "monitoring",
    "logging": "logging",
    "logging analytics": "logging_analytics",
    "apm": "apm",
    "db management": "db_management",
    "ops insights": "ops_insights",
    "notifications": "notifications",
    "events": "events",
    "auditing": "monitoring",  # uses monitoring icon as closest match
}

# Unicode circled numbers for flow badges ① ② ③ ... ⑳
_CIRCLED_NUMS = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"


class OCIDiagramGenerator:
    """Generate .drawio files using drawpyo for structure + custom icon injection.

    drawpyo handles: File, Page, Object (containers/groups), Edge (connections).
    Custom code handles: injecting multi-cell OCI stencil icons from oci-icons.json.
    """

    # Lazily loaded icon cache from kb/diagram/oci-icons.json
    ICON_CACHE = None

    @classmethod
    def _load_icon_cache(cls):
        """Load icon data from oci-icons.json if available."""
        if cls.ICON_CACHE is not None:
            return
        # Try multiple paths: relative to this script, then cwd
        candidates = [
            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         "..", "kb", "diagram", "oci-icons.json"),
            os.path.join("kb", "diagram", "oci-icons.json"),
        ]
        for path in candidates:
            if os.path.exists(path):
                with open(path, "r", encoding="utf-8") as f:
                    cls.ICON_CACHE = json.load(f)
                return
        # Not found — fall back to empty dict (colored rectangles)
        cls.ICON_CACHE = {}

    def __init__(self):
        self._load_icon_cache()
        self.file = drawpyo.File()
        self.page = drawpyo.Page(file=self.file)
        self._objects = {}        # cell_id (str) -> drawpyo Object
        self._abs_positions = {}  # cell_id -> (abs_x, abs_y) for relative→absolute conversion
        self._raw_cells = []      # raw mxCell XML strings for icon stencil cells
        self._edge_extras = {}    # drawpyo edge id -> extra style attrs string
        self._edge_objects = {}   # drawpyo edge id -> (source_id, target_id) for badge placement
        self._flow_badges = []    # list of (flow_order, edge_id) for deferred badge creation
        self._id_counter = 1000   # counter for auto-generated cell IDs
        # Track counts for summary
        self._container_count = 0
        self._service_count = 0
        self._connection_count = 0

    def _next_id(self) -> str:
        self._id_counter += 1
        return str(self._id_counter)

    @staticmethod
    def _escape(text: str) -> str:
        """Escape XML special characters and convert newlines."""
        if not text:
            return ""
        return (text
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace('"', "&quot;")
                .replace("\n", "&#xa;"))

    def _get_svc_style(self, service_type: str, font_size: Optional[int] = None) -> str:
        """Get the style string for a service type."""
        category = SVC_CATEGORY.get(service_type, "svc_infra")
        style = STYLES[category]
        if font_size:
            style = re.sub(r"fontSize=\d+", f"fontSize={font_size}", style)
        return style

    def _create_object(
        self, cell_id: str, label: str, style_str: str,
        parent_id: Optional[str], x: int, y: int, w: int, h: int,
    ) -> "drawpyo.diagram.Object":
        """Create a drawpyo Object with OCI style, register it, and return it.

        drawpyo uses ABSOLUTE coordinates for children, but our specs use
        RELATIVE coords (relative to parent). We convert by adding the
        parent's absolute position to the child's relative offset.
        """
        parent_obj = self._objects.get(parent_id) if parent_id else None

        # Convert relative (x, y) to absolute by adding parent's absolute position
        abs_x, abs_y = x, y
        if parent_obj:
            parent_abs = self._abs_positions.get(parent_id, (0, 0))
            abs_x = parent_abs[0] + x
            abs_y = parent_abs[1] + y

        if parent_obj:
            obj = drawpyo.diagram.Object(
                page=self.page, value=label, parent=parent_obj,
            )
        else:
            obj = drawpyo.diagram.Object(page=self.page, value=label)
        obj.apply_style_string(style_str)
        obj.position = (abs_x, abs_y)
        obj.width = w
        obj.height = h
        self._objects[cell_id] = obj
        # Store the absolute position for children to reference
        self._abs_positions[cell_id] = (abs_x, abs_y)
        return obj

    # ================================================================
    # Container methods
    # ================================================================

    def add_tenancy(
        self, cell_id: str, label: str,
        x: int = 30, y: int = 80, w: int = 1850, h: int = 720,
    ) -> str:
        """Add the outermost Tenancy container (dashed gray, no fill)."""
        self._create_object(cell_id, label, STYLES["tenancy"], None, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_region(
        self, cell_id: str, label: str, parent: str,
        x: int = 15, y: int = 30, w: int = 1140, h: int = 670,
    ) -> str:
        """Add a Region container (solid warm gray fill, rounded)."""
        self._create_object(cell_id, label, STYLES["region"], parent, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_ad(
        self, cell_id: str, label: str, parent: str,
        x: int = 15, y: int = 30, w: int = 1100, h: int = 640,
    ) -> str:
        """Add an Availability Domain container (medium gray fill)."""
        self._create_object(cell_id, label, STYLES["ad"], parent, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_vcn(
        self, cell_id: str, label: str, parent: str,
        x: int = 15, y: int = 30, w: int = 1100, h: int = 600,
    ) -> str:
        """Add a VCN container (dashed burnt orange border, no fill)."""
        self._create_object(cell_id, label, STYLES["vcn"], parent, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_subnet(
        self, cell_id: str, label: str, parent: str,
        x: int = 15, y: int = 30, w: int = 280, h: int = 400,
    ) -> str:
        """Add a Subnet container (dashed orange border, no fill)."""
        self._create_object(cell_id, label, STYLES["subnet"], parent, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_compartment(
        self, cell_id: str, label: str, parent: str,
        x: int = 15, y: int = 30, w: int = 400, h: int = 300,
    ) -> str:
        """Add a Compartment / Fault Domain / Tier container (dashed gray)."""
        self._create_object(cell_id, label, STYLES["compartment"], parent, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_onprem(
        self, cell_id: str, label: str,
        x: int = 30, y: int = 850, w: int = 650, h: int = 120,
    ) -> str:
        """Add an On-Premises container (outside tenancy, dashed gray)."""
        self._create_object(cell_id, label, STYLES["compartment"], None, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_cloud(
        self, cell_id: str, label: str,
        x: int = 10, y: int = 80, w: int = 200, h: int = 300,
        provider: str = "generic",
    ) -> str:
        """Add an external cloud container (AWS, Azure, GCP).

        provider: "azure" → solid blue border (Oracle multicloud style)
                  "gcp"   → solid green border
                  "aws"   → dashed muted border (default)
                  "generic" → dashed muted border
        """
        style_map = {
            "azure": "azure_container",
            "gcp": "gcp_container",
        }
        style_key = style_map.get(provider, "cloud_container")
        self._create_object(cell_id, label, STYLES[style_key], None, x, y, w, h)
        self._container_count += 1
        return cell_id

    def add_external(
        self, cell_id: str, label: str,
        x: int = 30, y: int = 30, w: int = 50, h: int = 80,
        icon: str = "user",
    ) -> str:
        """Add an external actor with icon.

        icon: "user"     — single person silhouette (circle head + body arc) in teal
              "users"    — two person silhouettes side by side (group icon)
              "internet" — cloud stencil
              "generic"  — colored rectangle
        """
        if icon in ("user", "users"):
            # Create an invisible group for connectivity + label
            self._create_object(cell_id, "", STYLES["external_user"], None, x, y, w, h)
            group_placeholder = f"__DRAWPYO_{cell_id}__"

            if icon == "users":
                # Two person silhouettes side by side (group icon)
                # Left person (slightly behind, offset left)
                cx_l = w // 2 - 10
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="ellipse;fillColor=none;strokeColor=#2D5967;strokeWidth=1.5;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx_l - 8}" y="2" width="16" height="16" as="geometry"/>'
                    f'</mxCell>'
                )
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="shape=mxgraph.basic.arc;dx=0.5;fillColor=none;'
                    f'strokeColor=#2D5967;strokeWidth=1.5;rotation=180;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx_l - 12}" y="20" width="24" height="16" as="geometry"/>'
                    f'</mxCell>'
                )
                # Right person (foreground, offset right)
                cx_r = w // 2 + 10
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="ellipse;fillColor=none;strokeColor=#2D5967;strokeWidth=2;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx_r - 9}" y="0" width="18" height="18" as="geometry"/>'
                    f'</mxCell>'
                )
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="shape=mxgraph.basic.arc;dx=0.5;fillColor=none;'
                    f'strokeColor=#2D5967;strokeWidth=2;rotation=180;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx_r - 13}" y="20" width="26" height="18" as="geometry"/>'
                    f'</mxCell>'
                )
            else:
                # Single person silhouette
                cx = w // 2
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="ellipse;fillColor=none;strokeColor=#2D5967;strokeWidth=2;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx - 11}" y="0" width="22" height="22" as="geometry"/>'
                    f'</mxCell>'
                )
                self._raw_cells.append(
                    f'<mxCell id="{self._next_id()}" value="" '
                    f'style="shape=mxgraph.basic.arc;dx=0.5;fillColor=none;'
                    f'strokeColor=#2D5967;strokeWidth=2;rotation=180;" '
                    f'vertex="1" parent="{group_placeholder}">'
                    f'<mxGeometry x="{cx - 16}" y="24" width="32" height="22" as="geometry"/>'
                    f'</mxCell>'
                )

            # Label below the icon
            label_id = f"{cell_id}_label"
            label_style = (
                "text;html=1;whiteSpace=wrap;align=center;verticalAlign=top;"
                "fontFamily=Oracle Sans;fontSize=11;fontColor=#312D2A;"
                "strokeColor=none;fillColor=none;"
            )
            self._create_object(
                label_id, label, label_style, cell_id,
                -10, 48, w + 20, 20,
            )
        else:
            style_key = f"external_{icon}" if f"external_{icon}" in STYLES else "external_generic"
            self._create_object(cell_id, label, STYLES[style_key], None, x, y, w, h)
        return cell_id

    # ================================================================
    # Service block methods
    # ================================================================

    @staticmethod
    def _is_icon_label_cell(style: str) -> bool:
        """Detect the label cell within an OCI Library icon."""
        return ("verticalAlign=middle" in style
                and ("align=center" in style or "align=left" in style)
                and "shape=stencil(" in style
                and "fillColor=none" in style)

    def add_service(
        self, cell_id: str, label: str, service_type: str, parent: str,
        x: int = 20, y: int = 35, w: int = 150, h: int = 50,
        font_size: Optional[int] = None,
    ) -> str:
        """Add a service block with icon (if available) or colored rectangle fallback."""
        icon_entry = self.ICON_CACHE.get(service_type) if self.ICON_CACHE else None
        if icon_entry:
            return self._add_service_with_icon(
                cell_id, label, service_type, parent, icon_entry,
                x, y, w, h, font_size,
            )
        # Fallback: colored rectangle via drawpyo Object
        style = self._get_svc_style(service_type, font_size)
        self._create_object(cell_id, label, style, parent, x, y, w, h)
        self._service_count += 1
        return cell_id

    def _add_service_with_icon(
        self, cell_id: str, label: str, service_type: str, parent: str,
        icon_entry: dict,
        x: int, y: int, w: int, h: int,
        font_size: Optional[int] = None,
    ) -> str:
        """Embed official OCI icon cells inside a drawpyo group Object.

        Architecture:
        - The group Object (cell_id) covers ONLY the icon area — its height equals
          the scaled icon height, NOT the full service block height.  This ensures
          draw.io routes connection arrows to the icon boundary, not the label area.
        - The label is a SIBLING of the icon group (child of the same parent), placed
          immediately below the group.  It does not affect connection geometry.

        Benefit: connections from/to services visually terminate at the icon itself,
        which matches Oracle Architecture Center reference diagram style.
        """
        icon_w = icon_entry["w"]
        icon_h = icon_entry["h"]
        icon_cells = icon_entry["cells"]

        # Label height: proportional to actual line count (no over-allocation).
        # ~15px per line at 11pt Oracle Sans, minimum 20px for single-line labels.
        n_lines = label.count('\n') + 1
        label_h = max(n_lines * 16 + 4, 20)

        # Icon area = full spec height minus label height
        icon_area_h = max(h - label_h, 20)

        # Scale icon to fit the icon area (never upscale beyond original size)
        scale = min(w / icon_w, icon_area_h / icon_h, 1.0)
        scaled_w = icon_w * scale
        scaled_h = icon_h * scale

        # Icon group height = scaled icon height only.
        # Connections terminate here — at the visible icon boundary.
        icon_group_h = max(int(scaled_h), 20)

        group_style = (
            "group;fillColor=none;strokeColor=none;pointerEvents=1;"
            "connectable=1;"
        )
        self._create_object(cell_id, "", group_style, parent, x, y, w, icon_group_h)

        # Build ID mapping: original cell id -> unique cell id
        original_ids = set()
        for cell_xml in icon_cells:
            cell_elem = ET.fromstring(cell_xml)
            original_ids.add(cell_elem.get("id"))

        id_map = {}
        for orig_id in sorted(original_ids,
                              key=lambda x_: int(x_) if x_.isdigit() else 0):
            id_map[orig_id] = f"{cell_id}_i{orig_id}"

        # Center the icon horizontally within the group
        icon_offset_x = (w - scaled_w) / 2

        # Use a placeholder for the group's drawpyo ID — replaced in to_xml()
        group_placeholder = f"__DRAWPYO_{cell_id}__"

        # Emit icon cells (skip the library's built-in label cell)
        for cell_xml in icon_cells:
            cell_elem = ET.fromstring(cell_xml)
            orig_id = cell_elem.get("id")
            orig_parent = cell_elem.get("parent", "1")
            style = cell_elem.get("style", "")

            # Skip the icon's built-in label cell — we replace with our own
            if self._is_icon_label_cell(style):
                continue

            new_id = id_map[orig_id]

            # Remap parent
            if orig_parent == "1":
                new_parent = group_placeholder
            elif orig_parent in id_map:
                new_parent = id_map[orig_parent]
            else:
                new_parent = group_placeholder

            cell_elem.set("id", new_id)
            cell_elem.set("parent", new_parent)

            # NOTE: Stencil icons are ALWAYS teal (#2D5967) per OCI visual style.
            # Copper (#AA643B) is only for fallback rectangles (no icon available).

            # Scale and offset geometry
            geo = cell_elem.find("mxGeometry")
            if geo is not None:
                for attr in ("x", "y", "width", "height"):
                    val = geo.get(attr)
                    if val is not None:
                        try:
                            geo.set(attr, str(float(val) * scale))
                        except ValueError:
                            pass
                # Offset x for centering (only top-level icon cells parented to group)
                if orig_parent == "1":
                    cur_x = float(geo.get("x", "0"))
                    geo.set("x", str(cur_x + icon_offset_x))

            cell_str = ET.tostring(cell_elem, encoding="unicode")
            self._raw_cells.append(cell_str)

        # Label is a SIBLING of the icon group (child of `parent`, not of `cell_id`).
        # This keeps connection geometry clean: arrows connect to the icon group only.
        # Oracle ref arch style: 11pt, charcoal (#312D2A), centered below icon.
        fs = font_size or 11
        label_style = (
            f"text;html=1;whiteSpace=wrap;overflow=visible;align=center;verticalAlign=top;"
            f"fontFamily=Oracle Sans;fontSize={fs};fontColor=#312D2A;"
            f"strokeColor=none;fillColor=none;spacingTop=2;"
        )
        # Label width = icon width (centered); overflow=visible handles longer text.
        label_w = w
        label_id = f"{cell_id}_label"
        self._create_object(
            label_id, label, label_style, parent,
            x, y + icon_group_h + 2, label_w, label_h,
        )

        self._service_count += 1
        return cell_id

    def add_obs_bar(
        self, cell_id: str, label: str, parent: str,
        x: int = 20, y: int = 640, w: int = 78, h: int = 22,
    ) -> str:
        """Add an observability bar element (small teal pill)."""
        self._create_object(cell_id, label, STYLES["obs_bar"], parent, x, y, w, h)
        return cell_id

    # ================================================================
    # NSG corner badge — Oracle visual signature
    # ================================================================

    # ================================================================
    # Connection methods
    # ================================================================

    def add_connection(
        self, cell_id: str, label: Optional[str], conn_type: str,
        source: str, target: str,
        waypoints: Optional[list] = None,
        flow_order: Optional[int] = None,
    ) -> str:
        """Add a connection arrow between two elements using drawpyo Edge.

        flow_order: if set (1-20), renders a teal circle badge with the number
        on the midpoint of the edge for visual storytelling.
        """
        src_obj = self._objects.get(source)
        tgt_obj = self._objects.get(target)
        if not src_obj or not tgt_obj:
            # Source or target not found — skip silently
            return cell_id

        edge = drawpyo.diagram.Edge(
            page=self.page, source=src_obj, target=tgt_obj,
            label=label or "",
        )

        # Base OCI style: open arrows, charcoal color, orthogonal routing
        edge.line_end_target = "open"
        edge.line_end_source = "none"
        edge.strokeColor = "#312D2A"
        edge.strokeWidth = 1
        edge.rounded = True
        edge.endSize = 6
        edge.waypoints = "orthogonal"

        # Font styling via text_format (drawpyo's supported approach)
        edge.text_format.fontFamily = "Oracle Sans"
        edge.text_format.fontSize = 10
        edge.text_format.fontColor = "#312D2A"
        edge.text_format.labelBackgroundColor = "#FFFFFF"

        # Extra style attributes that drawpyo Edge doesn't support natively.
        # labelBackgroundColor: white so edge labels are readable and the line
        # is hidden behind the text (confirmed in Oracle ref arch diagrams).
        # labelBorderColor=none: no border box around the label.
        extra_style = (
            "elbow=vertical;endFill=0;startFill=0;"
            "labelBackgroundColor=#FFFFFF;labelBorderColor=none;"
            "fontFamily=Oracle Sans;fontSize=10;fontColor=#312D2A;"
        )

        # Customize per connection type
        if conn_type == "fastconnect":
            # Bidirectional
            edge.line_end_source = "open"
        elif conn_type == "adg":
            # Dashed (ADG replication) — toolkit pattern: 8 4
            edge.pattern = "dashed_small"
            extra_style += "dashPattern=8 4;"
        elif conn_type == "migration":
            # Dashed (migration) — toolkit pattern: 8 4
            edge.pattern = "dashed_medium"
            extra_style += "dashPattern=8 4;"
        elif conn_type == "etl":
            # Dashed (ETL/streaming) — toolkit pattern: 6 4
            edge.pattern = "dashed_small"
            extra_style += "dashPattern=6 4;"
        # "standard" and "db" use solid line defaults — no changes needed

        # Add waypoints if provided
        if waypoints:
            for wx, wy in waypoints:
                edge.add_point(wx, wy)

        # Store extra style for post-processing (keyed by drawpyo's edge id)
        self._edge_extras[str(edge.id)] = extra_style

        # Track edge endpoints for flow badge placement
        self._edge_objects[str(edge.id)] = (source, target)

        # Queue flow badge if requested
        if flow_order is not None and 1 <= flow_order <= 20:
            self._flow_badges.append((flow_order, source, target))

        self._objects[cell_id] = src_obj  # register for potential referencing
        self._connection_count += 1
        return cell_id

    # ================================================================
    # Flow badges — numbered circles on connection midpoints
    # ================================================================

    def _create_flow_badges(self):
        """Create teal circle badges with flow numbers near connection sources.

        Called after all objects and connections are registered, before save.
        Badge is positioned at 25% of the path from source (not midpoint) to
        avoid overlapping with destination icons.  A small perpendicular offset
        prevents badges from sitting on top of the arrow line itself.
        """
        badge_size = 18
        for flow_order, source_id, target_id in self._flow_badges:
            src_pos = self._abs_positions.get(source_id)
            tgt_pos = self._abs_positions.get(target_id)
            if not src_pos or not tgt_pos:
                continue

            src_obj = self._objects.get(source_id)
            tgt_obj = self._objects.get(target_id)
            if not src_obj or not tgt_obj:
                continue

            # Compute center of source and target
            src_cx = src_pos[0] + src_obj.width / 2
            src_cy = src_pos[1] + src_obj.height / 2
            tgt_cx = tgt_pos[0] + tgt_obj.width / 2
            tgt_cy = tgt_pos[1] + tgt_obj.height / 2

            # Position at 25% from source (closer to source, avoids target overlap)
            t = 0.25
            mid_x = int(src_cx + (tgt_cx - src_cx) * t - badge_size / 2)
            mid_y = int(src_cy + (tgt_cy - src_cy) * t - badge_size / 2)

            # Small perpendicular offset to avoid sitting on the arrow line
            dx = tgt_cx - src_cx
            dy = tgt_cy - src_cy
            length = max((dx**2 + dy**2) ** 0.5, 1)
            # Offset 14px perpendicular to the connection direction
            offset_x = int(-dy / length * 14)
            offset_y = int(dx / length * 14)
            mid_x += offset_x
            mid_y += offset_y

            # Use circled number character or plain number
            if flow_order <= len(_CIRCLED_NUMS):
                badge_label = str(flow_order)
            else:
                badge_label = str(flow_order)

            badge_id = f"flow_{flow_order}"
            self._create_object(
                badge_id, badge_label, STYLES["flow_badge"],
                None, mid_x, mid_y, badge_size, badge_size,
            )

    # ================================================================
    # Title
    # ================================================================

    def add_title(
        self, text: str,
        x: int = 30, y: int = 10, w: int = 800, h: int = 30,
    ) -> str:
        """Add an italic title label."""
        self._create_object("title", text, STYLES["title"], None, x, y, w, h)
        return "title"

    # ================================================================
    # Output — merge drawpyo XML + injected raw icon cells
    # ================================================================

    def to_xml(self) -> str:
        """Generate the complete .drawio XML.

        1. Get drawpyo's XML via file.xml property
        2. Replace drawpyo's default mxfile/diagram attributes with OCI ones
        3. Inject extra style attributes into edge cells
        4. Inject raw icon stencil cells before </root>
        5. Replace group parent placeholders with actual drawpyo IDs
        6. Sanitize: replace #ffffff with #FCFBFA (no pure white in OCI palette)
        """
        xml = self.file.xml

        # Replace drawpyo's default mxfile attributes with OCI-branded ones
        xml = re.sub(
            r'<mxfile[^>]*>',
            '<mxfile host="app.diagrams.net" '
            'agent="OCI Deal Accelerator" version="24.0.0" type="device">',
            xml,
        )

        # Replace drawpyo's default diagram attributes
        xml = re.sub(
            r'<diagram[^>]*>',
            '<diagram name="OCI Architecture" id="oci-arch">',
            xml,
        )

        # Canvas: white background so region fill (#F0EDE9) is clearly visible
        xml = re.sub(
            r'<mxGraphModel[^>]*>',
            '<mxGraphModel dx="1900" dy="1200" grid="1" gridSize="10" '
            'guides="1" tooltips="1" connect="1" arrows="1" fold="1" '
            'page="1" pageScale="1" pageWidth="1900" pageHeight="1200" '
            'background="#FFFFFF" math="0" shadow="0">',
            xml,
        )

        # Inject extra style attributes into edge cells (e.g., elbow, dashPattern)
        for edge_id, extra_style in self._edge_extras.items():
            # Find the edge cell by its drawpyo ID and append extra styles
            pattern = f'id="{edge_id}" style="'
            if pattern in xml:
                xml = xml.replace(pattern, f'id="{edge_id}" style="{extra_style}')

        # Inject raw icon stencil cells before </root>
        if self._raw_cells:
            inject = "\n".join(f"        {c}" for c in self._raw_cells)
            xml = xml.replace("</root>", inject + "\n      </root>")

        # Replace placeholder parent references with actual drawpyo IDs
        for cell_id, obj in self._objects.items():
            placeholder = f"__DRAWPYO_{cell_id}__"
            if placeholder in xml:
                xml = xml.replace(placeholder, str(obj.id))

        # Sanitize: no pure white in OCI palette
        xml = xml.replace('fill="#ffffff"', 'fill="#FCFBFA"')
        xml = xml.replace('fill="#FFFFFF"', 'fill="#FCFBFA"')
        xml = xml.replace("fillColor=#ffffff", "fillColor=#FCFBFA")
        xml = xml.replace("fillColor=#FFFFFF", "fillColor=#FCFBFA")

        # Fix drawpyo's default dash patterns to match OCI toolkit
        # drawpyo emits dashed=1 without dashPattern → draw.io defaults to 3 3
        # We ensure all dashed containers get 6 4 and dashed edges get 8 4
        # (Edge dashPatterns are already injected via _edge_extras above)

        return xml

    def save(self, filepath: str):
        """Save to a .drawio file."""
        # Create flow badges before generating XML
        # self._create_flow_badges()  # disabled — Oracle style uses dash patterns, not numbered badges
        xml = self.to_xml()
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(xml)

    # ================================================================
    # High-level: build from YAML spec
    # ================================================================

    @classmethod
    def from_spec(cls, spec: dict) -> "OCIDiagramGenerator":
        """Build a diagram from a YAML specification.

        The spec follows the OCI visual hierarchy:

            tenancy -> region(s) -> vcn(s) -> subnet(s) -> service(s)

        Plus optional on-premises, external actors, and connections.
        See examples/diagram-spec.yaml for the full format.
        """
        gen = cls()

        # External actors (users, internet, third-party)
        for ext in spec.get("external", []):
            ext_id = ext.get("id", gen._next_id())
            gen.add_external(
                ext_id,
                ext["label"].replace("\\n", "\n"),
                x=ext.get("x", 30), y=ext.get("y", 30),
                w=ext.get("w", 50), h=ext.get("h", 60),
                icon=ext.get("icon", "user"),
            )

        # External cloud containers (AWS, Azure, GCP)
        for cloud in spec.get("clouds", []):
            cloud_id = cloud.get("id", gen._next_id())
            gen.add_cloud(
                cloud_id,
                cloud["label"].replace("\\n", "\n"),
                x=cloud.get("x", 10), y=cloud.get("y", 80),
                w=cloud.get("w", 200), h=cloud.get("h", 300),
                provider=cloud.get("provider", "generic"),
            )
            # Services inside the cloud container.
            # cy=60: enough vertical room for 2-line cloud container titles at 12pt.
            # Services are centered horizontally within the cloud container.
            cloud_w = cloud.get("w", 200)
            cy = 60
            for svc in cloud.get("services", []):
                svc_id = svc.get("id", gen._next_id())
                svc_w = svc.get("w", 100)
                svc_h = svc.get("h", 80)
                # Center each service horizontally in the cloud container
                cx = max(10, (cloud_w - svc_w) // 2)
                label = svc["label"].replace("\\n", "\n")
                cloud_icon = svc.get("cloud_icon")
                if cloud_icon:
                    # Use cloud provider icon (AWS/GCP) via stencil style
                    style = _aws_icon_style(cloud_icon) or _gcp_icon_style(cloud_icon)
                    if style:
                        gen._create_object(
                            svc_id, label, style, cloud_id,
                            cx, cy, svc_w, svc_h,
                        )
                        gen._service_count += 1
                    else:
                        gen.add_service(svc_id, label, svc.get("type", "compute"),
                                        cloud_id, cx, cy, svc_w, svc_h)
                else:
                    gen.add_service(svc_id, label, svc.get("type", "compute"),
                                    cloud_id, cx, cy, svc_w, svc_h)
                cy += svc_h + 20

        # Tenancy
        tenancy_spec = spec.get("tenancy", {})
        tenancy_x = tenancy_spec.get("x", 30)
        tenancy_y = tenancy_spec.get("y", 80)
        tenancy_w = tenancy_spec.get("w", 1850)
        tenancy_h = tenancy_spec.get("h", 720)
        gen.add_tenancy(
            "tenancy",
            tenancy_spec.get("label", "Oracle Cloud Infrastructure"),
            x=tenancy_x, y=tenancy_y, w=tenancy_w, h=tenancy_h,
        )

        # Regions
        for region in tenancy_spec.get("regions", []):
            is_primary = region.get("primary", False)
            rx = region.get("x", 15 if is_primary else 1195)
            # ry default 40: ~18px below tenancy label (12pt bold) + comfortable padding
            ry = region.get("y", 40)
            rw = region.get("w", 1140 if is_primary else 640)
            rh = region.get("h", 670 if is_primary else 480)

            gen.add_region(
                region["id"],
                region["label"],
                "tenancy", rx, ry, rw, rh,
            )


            # Availability Domains (optional)
            for ad in region.get("availability_domains", []):
                gen.add_ad(
                    ad["id"], ad["label"], region["id"],
                    x=ad.get("x", 15), y=ad.get("y", 30),
                    w=ad.get("w", rw - 30), h=ad.get("h", rh - 70),
                )

            # VCNs
            for vcn in region.get("vcns", []):
                vcn_parent = vcn.get("parent", region["id"])
                vcn_w = vcn.get("w", rw - 30)
                vcn_h = vcn.get("h", rh - 70)
                gen.add_vcn(
                    vcn["id"],
                    f"VCN {vcn['label']}",
                    vcn_parent,
                    # y default 45: leaves proper gap below the region label (12pt ≈22px + padding)
                    x=vcn.get("x", 15), y=vcn.get("y", 45),
                    w=vcn_w, h=vcn_h,
                )

                # Auto-layout subnets VERTICALLY (stacked top-to-bottom).
                # Matches Oracle reference architecture style: subnets span the
                # full VCN width and stack vertically, one on top of another.
                # (Confirmed from 11 Oracle Architecture Center reference diagrams)
                # subnet_y=32: leaves ~10px gap below the VCN label (12pt bold ≈22px).
                subnet_y = 32
                for subnet in vcn.get("subnets", []):
                    # Default width = full VCN width minus 16px padding (8px each side)
                    sw = subnet.get("w", vcn_w - 16)
                    sh = subnet.get("h", 120)
                    gen.add_subnet(
                        subnet["id"], subnet["label"],
                        vcn["id"], 8, subnet_y, sw, sh,
                    )

                    # Auto-layout services HORIZONTALLY within subnet, centered.
                    # Oracle style: services are centered left-to-right within each
                    # subnet row, not left-aligned. Single service → centered in subnet.
                    svc_top = 30  # vertical offset below subnet label
                    subnet_svcs = subnet.get("services", [])
                    # Compute each service width upfront to center the group
                    svc_widths = [svc.get("w", min(sw - 30, 150)) for svc in subnet_svcs]
                    total_svc_w = sum(svc_widths) + 16 * max(0, len(svc_widths) - 1)
                    # Center the service group; minimum 15px left padding
                    svc_x = max(15, (sw - total_svc_w) // 2)
                    for svc, svc_w in zip(subnet_svcs, svc_widths):
                        svc_id = svc.get("id", gen._next_id())
                        svc_h = svc.get("h", 80)
                        label = svc["label"].replace("\\n", "\n")
                        gen.add_service(
                            svc_id, label, svc["type"],
                            subnet["id"], svc_x, svc_top, svc_w, svc_h,
                            font_size=svc.get("fontSize"),
                        )
                        svc_x += svc_w + 16

                    subnet_y += sh + 14

                # Gateways on left side of VCN, stacked vertically
                gx = 10
                gy = 40
                for gw in vcn.get("gateways", []):
                    gw_id = gw.get("id", gen._next_id())
                    gw_w = gw.get("w", 110)
                    gw_h = gw.get("h", 70)
                    gw_x = gw.get("x", gx)
                    gw_y = gw.get("y", gy)
                    label = gw["label"].replace("\\n", "\n")
                    gen.add_service(
                        gw_id, label, gw["type"],
                        vcn["id"], gw_x, gw_y, gw_w, gw_h,
                    )
                    gy = gw_y + gw_h + 8

            # Compartments inside region — can contain VCNs
            for comp in region.get("compartments", []):
                comp_id = comp.get("id", gen._next_id())
                comp_w = comp.get("w", rw - 20)
                comp_h = comp.get("h", 60)
                gen.add_compartment(
                    comp_id, comp["label"],
                    region["id"],
                    x=comp.get("x", 10), y=comp.get("y", rh - 80),
                    w=comp_w, h=comp_h,
                )
                # VCNs inside compartment
                for vcn in comp.get("vcns", []):
                    vcn_parent = comp_id
                    vcn_w = vcn.get("w", comp_w - 20)
                    vcn_h = vcn.get("h", comp_h - 50)
                    gen.add_vcn(
                        vcn["id"],
                        f"VCN {vcn['label']}",
                        vcn_parent,
                        x=vcn.get("x", 10), y=vcn.get("y", 45),
                        w=vcn_w, h=vcn_h,
                    )
                    subnet_y = 32
                    for subnet in vcn.get("subnets", []):
                        sw = subnet.get("w", vcn_w - 16)
                        sh = subnet.get("h", 120)
                        gen.add_subnet(
                            subnet["id"], subnet["label"],
                            vcn["id"], 8, subnet_y, sw, sh,
                        )
                        svc_top = 30
                        comp_svcs = subnet.get("services", [])
                        comp_svc_ws = [svc.get("w", min(sw - 30, 150)) for svc in comp_svcs]
                        total_csvc_w = sum(comp_svc_ws) + 12 * max(0, len(comp_svc_ws) - 1)
                        svc_x = max(15, (sw - total_csvc_w) // 2)
                        for svc, svc_w in zip(comp_svcs, comp_svc_ws):
                            svc_id = svc.get("id", gen._next_id())
                            svc_h = svc.get("h", 80)
                            label = svc["label"].replace("\\n", "\n")
                            gen.add_service(
                                svc_id, label, svc["type"],
                                subnet["id"], svc_x, svc_top, svc_w, svc_h,
                                font_size=svc.get("fontSize"),
                            )
                            svc_x += svc_w + 16
                        subnet_y += sh + 14
                    # Gateways on left side of compartment VCN, stacked vertically
                    gx = 10
                    gy = 40
                    for gw in vcn.get("gateways", []):
                        gw_id = gw.get("id", gen._next_id())
                        gw_w = gw.get("w", 95)
                        gw_h = gw.get("h", 60)
                        gw_x = gw.get("x", gx)
                        gw_y = gw.get("y", gy)
                        label = gw["label"].replace("\\n", "\n")
                        gen.add_service(
                            gw_id, label, gw["type"],
                            vcn["id"], gw_x, gw_y, gw_w, gw_h,
                        )
                        gy = gw_y + gw_h + 8

            # Observability row at bottom of region — uses icons (only if explicitly requested)
            if region.get("observability"):
                obs_icon_w = 90
                obs_icon_h = 80
                ox = 20
                oy = rh - obs_icon_h - 8
                for obs_label in region["observability"]:
                    obs_id = gen._next_id()
                    obs_type = OBS_TYPE_MAP.get(obs_label.lower(), None)
                    if obs_type:
                        gen.add_service(
                            obs_id, obs_label, obs_type,
                            region["id"], ox, oy, obs_icon_w, obs_icon_h,
                            font_size=10,
                        )
                    else:
                        gen.add_obs_bar(
                            obs_id, obs_label, region["id"],
                            ox, oy + obs_icon_h - 22, 80, 22,
                        )
                    ox += obs_icon_w + 8

        # On-premises
        if spec.get("onprem"):
            op = spec["onprem"]
            op_x = op.get("x", 30)
            op_y = op.get("y", tenancy_y + tenancy_h + 20)
            op_w = op.get("w", 750)
            op_h = op.get("h", 120)
            gen.add_onprem("onprem", op.get("label", "On-Premises Data Center"),
                           x=op_x, y=op_y, w=op_w, h=op_h)
            opx = 20
            for svc in op.get("services", []):
                svc_id = svc.get("id", gen._next_id())
                svc_w = svc.get("w", 180)
                svc_h = svc.get("h", 60)
                label = svc["label"].replace("\\n", "\n")
                gen.add_service(
                    svc_id, label, svc["type"],
                    "onprem", opx, 30, svc_w, svc_h,
                )
                opx += svc_w + 12

        # Connections
        for conn in spec.get("connections", []):
            conn_id = conn.get("id", gen._next_id())
            gen.add_connection(
                conn_id, conn.get("label"), conn["type"],
                conn["from"], conn["to"],
                waypoints=conn.get("waypoints"),
                flow_order=conn.get("flow_order"),
            )

        # Title — centered across the full diagram width
        if spec.get("title"):
            total_w = tenancy_x + tenancy_w
            gen.add_title(spec["title"], x=0, y=8, w=total_w, h=35)

        return gen


# ============================================================
# CLI
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="Generate OCI architecture .drawio diagrams from YAML specs"
    )
    parser.add_argument(
        "--spec", required=True,
        help="Path to YAML spec file describing the architecture",
    )
    parser.add_argument(
        "--output", default="architecture.drawio",
        help="Output .drawio file path (default: architecture.drawio)",
    )
    args = parser.parse_args()

    with open(args.spec, 'r') as f:
        spec = yaml.safe_load(f)

    gen = OCIDiagramGenerator.from_spec(spec)
    gen.save(args.output)

    # Print summary
    print(f"Generated: {args.output}")
    print(f"  Containers: {gen._container_count}")
    print(f"  Service blocks: {gen._service_count}")
    print(f"  Connections: {gen._connection_count}")
    print(f"  Flow badges: {len(gen._flow_badges)}")
    print(f"  Raw icon cells: {len(gen._raw_cells)}")
    print(f"  Registered objects: {len(gen._objects)}")


if __name__ == "__main__":
    main()
