"""
Connector Parser - Extract shapes and connectors from Excel drawing XML.

Uses position-based detection to infer connections between shapes.
"""

import logging
import xml.etree.ElementTree as ET
import zipfile
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
import math

logger = logging.getLogger(__name__)


@dataclass
class ShapeInfo:
    """Information about a shape in the drawing."""
    id: str
    name: str
    text: str
    shape_type: str
    position: Tuple[int, int]  # (x, y) top-left
    size: Tuple[int, int]      # (width, height)
    center: Tuple[int, int]    # (center_x, center_y)
    sheet_name: str
    fill_color: Optional[str] = None  # RGB hex color (e.g. "000000" for black)


@dataclass
class ConnectorInfo:
    """Information about a connector (arrow) in the drawing."""
    id: str
    name: str
    start_pos: Tuple[int, int]  # (x, y)
    end_pos: Tuple[int, int]    # (x, y)
    text: str  # Label on the connector
    has_arrow: bool
    sheet_name: str


class ConnectorParser:
    """
    Parse Excel drawing XML to extract shapes and connectors.

    Uses position-based detection to infer which shapes are connected.
    """

    # XML namespaces
    NS = {
        'xdr': 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing',
        'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'
    }

    def __init__(self):
        """Initialize the connector parser."""
        self.logger = logging.getLogger(__name__)

    def parse_from_xlsx(self, xlsx_path: str) -> Tuple[List[ShapeInfo], List[ConnectorInfo]]:
        """
        Parse shapes and connectors from an Excel .xlsx file.

        Args:
            xlsx_path: Path to .xlsx file

        Returns:
            Tuple of (shapes, connectors)
        """
        try:
            with zipfile.ZipFile(xlsx_path, 'r') as zf:
                # Find all drawing XML files
                drawing_files = [f for f in zf.namelist()
                                if 'xl/drawings/' in f and f.endswith('.xml')]

                all_shapes = []
                all_connectors = []

                for drawing_file in drawing_files:
                    # Determine sheet name from drawing relationships
                    sheet_name = self._get_sheet_name_for_drawing(zf, drawing_file)

                    # Parse the drawing XML
                    xml_content = zf.read(drawing_file).decode('utf-8')
                    shapes, connectors = self._parse_drawing_xml(xml_content, sheet_name)

                    all_shapes.extend(shapes)
                    all_connectors.extend(connectors)

                self.logger.info(
                    f"Parsed {len(all_shapes)} shapes and {len(all_connectors)} connectors "
                    f"from {len(drawing_files)} drawing file(s)"
                )

                return all_shapes, all_connectors

        except Exception as e:
            self.logger.error(f"Error parsing connectors from xlsx: {e}")
            return [], []

    def _get_sheet_name_for_drawing(self, zf: zipfile.ZipFile, drawing_file: str) -> str:
        """
        Get the sheet name associated with a drawing file.

        Args:
            zf: ZipFile object
            drawing_file: Path to drawing XML file

        Returns:
            Sheet name or "Unknown"
        """
        # For now, return a default name
        # In production, parse sheet relationships to get actual name
        return "Sheet1"

    def _parse_drawing_xml(
        self,
        xml_content: str,
        sheet_name: str
    ) -> Tuple[List[ShapeInfo], List[ConnectorInfo]]:
        """
        Parse shapes and connectors from drawing XML content.

        Args:
            xml_content: XML content as string
            sheet_name: Name of the sheet

        Returns:
            Tuple of (shapes, connectors)
        """
        try:
            root = ET.fromstring(xml_content)

            # Parse shapes
            shapes = self._parse_shapes(root, sheet_name)

            # Parse connectors
            connectors = self._parse_connectors(root, sheet_name)

            return shapes, connectors

        except Exception as e:
            self.logger.error(f"Error parsing drawing XML: {e}")
            return [], []

    def _parse_shapes(self, root: ET.Element, sheet_name: str) -> List[ShapeInfo]:
        """
        Parse regular shapes (rectangles, ovals, etc.) from XML.

        Args:
            root: XML root element
            sheet_name: Name of the sheet

        Returns:
            List of ShapeInfo objects
        """
        shapes = []

        # Find all shape elements (not connectors)
        shape_elements = root.findall('.//xdr:sp', self.NS)

        for shape_elem in shape_elements:
            try:
                # Get ID and name
                cNvPr = shape_elem.find('.//xdr:cNvPr', self.NS)
                if cNvPr is None:
                    continue

                shape_id = cNvPr.get('id', '')
                shape_name = cNvPr.get('name', '')

                # Get position and size
                xfrm = shape_elem.find('.//a:xfrm', self.NS)
                if xfrm is None:
                    continue

                off = xfrm.find('.//a:off', self.NS)
                ext = xfrm.find('.//a:ext', self.NS)

                if off is None or ext is None:
                    continue

                x = int(off.get('x', '0'))
                y = int(off.get('y', '0'))
                width = int(ext.get('cx', '0'))
                height = int(ext.get('cy', '0'))

                # Calculate center
                center_x = x + width // 2
                center_y = y + height // 2

                # Get shape type
                prstGeom = shape_elem.find('.//a:prstGeom', self.NS)
                shape_type = prstGeom.get('prst', 'unknown') if prstGeom is not None else 'unknown'

                # Get text content
                text = self._extract_text_from_element(shape_elem)

                # Get fill color
                fill_color = self._extract_fill_color(shape_elem)

                shape = ShapeInfo(
                    id=shape_id,
                    name=shape_name,
                    text=text,
                    shape_type=shape_type,
                    position=(x, y),
                    size=(width, height),
                    center=(center_x, center_y),
                    sheet_name=sheet_name,
                    fill_color=fill_color
                )

                shapes.append(shape)

            except Exception as e:
                self.logger.debug(f"Error parsing shape: {e}")
                continue

        self.logger.debug(f"Parsed {len(shapes)} shapes")
        return shapes

    def _parse_connectors(self, root: ET.Element, sheet_name: str) -> List[ConnectorInfo]:
        """
        Parse connector shapes (arrows, lines) from XML.

        Args:
            root: XML root element
            sheet_name: Name of the sheet

        Returns:
            List of ConnectorInfo objects
        """
        connectors = []

        # Find all connector elements
        connector_elements = root.findall('.//xdr:cxnSp', self.NS)

        for conn_elem in connector_elements:
            try:
                # Get ID and name
                cNvPr = conn_elem.find('.//xdr:cNvPr', self.NS)
                if cNvPr is None:
                    continue

                conn_id = cNvPr.get('id', '')
                conn_name = cNvPr.get('name', '')

                # Get position and size
                xfrm = conn_elem.find('.//a:xfrm', self.NS)
                if xfrm is None:
                    continue

                off = xfrm.find('.//a:off', self.NS)
                ext = xfrm.find('.//a:ext', self.NS)

                if off is None or ext is None:
                    continue

                x = int(off.get('x', '0'))
                y = int(off.get('y', '0'))
                width = int(ext.get('cx', '0'))
                height = int(ext.get('cy', '0'))

                # Check for flip to determine direction
                flip_h = xfrm.get('flipH') == '1'
                flip_v = xfrm.get('flipV') == '1'

                # Calculate start and end positions
                if flip_h:
                    # Flipped horizontally - right to left
                    start_pos = (x + width, y)
                    end_pos = (x, y + height)
                else:
                    # Normal - left to right
                    start_pos = (x, y)
                    end_pos = (x + width, y + height)

                # Check for arrow head
                tailEnd = conn_elem.find('.//a:tailEnd', self.NS)
                headEnd = conn_elem.find('.//a:headEnd', self.NS)
                has_arrow = (tailEnd is not None or headEnd is not None)

                # Get text on connector
                text = self._extract_text_from_element(conn_elem)

                connector = ConnectorInfo(
                    id=conn_id,
                    name=conn_name,
                    start_pos=start_pos,
                    end_pos=end_pos,
                    text=text,
                    has_arrow=has_arrow,
                    sheet_name=sheet_name
                )

                connectors.append(connector)

            except Exception as e:
                self.logger.debug(f"Error parsing connector: {e}")
                continue

        self.logger.debug(f"Parsed {len(connectors)} connectors")
        return connectors

    def _extract_text_from_element(self, element: ET.Element) -> str:
        """
        Extract text content from a shape or connector element.

        Args:
            element: XML element

        Returns:
            Extracted text or empty string
        """
        text_parts = []

        # Find all text elements
        for t_elem in element.findall('.//a:t', self.NS):
            if t_elem.text:
                text_parts.append(t_elem.text.strip())

        return ' '.join(text_parts)

    def _extract_fill_color(self, element: ET.Element) -> Optional[str]:
        """
        Extract fill color from a shape element.

        Args:
            element: XML element (shape)

        Returns:
            RGB hex color (e.g. "000000" for black) or None if no solid fill
        """
        try:
            # Find shape properties
            spPr = element.find('.//xdr:spPr', self.NS)
            if spPr is None:
                return None

            # Look for solid fill
            solidFill = spPr.find('.//a:solidFill', self.NS)
            if solidFill is None:
                return None

            # Try to get RGB color
            srgbClr = solidFill.find('.//a:srgbClr', self.NS)
            if srgbClr is not None:
                return srgbClr.get('val')

            # Try to get scheme color (fallback)
            schemeClr = solidFill.find('.//a:schemeClr', self.NS)
            if schemeClr is not None:
                return schemeClr.get('val')

            return None

        except Exception as e:
            self.logger.debug(f"Error extracting fill color: {e}")
            return None

    def find_connected_shapes(
        self,
        shapes: List[ShapeInfo],
        connectors: List[ConnectorInfo],
        max_distance: int = 500000  # EMUs (1 EMU = 1/914400 inch)
    ) -> List[Tuple[str, str, str]]:
        """
        Infer connections between shapes based on connector positions.

        Uses spatial proximity: finds the closest shape to each end of a connector.

        Args:
            shapes: List of shapes
            connectors: List of connectors
            max_distance: Maximum distance to consider a connection (in EMUs)

        Returns:
            List of tuples: (from_shape_id, to_shape_id, connector_label)
        """
        connections = []

        for connector in connectors:
            # Find shape closest to connector start
            start_shape = self._find_closest_shape(
                connector.start_pos,
                shapes,
                max_distance
            )

            # Find shape closest to connector end
            end_shape = self._find_closest_shape(
                connector.end_pos,
                shapes,
                max_distance
            )

            if start_shape and end_shape and start_shape != end_shape:
                # Found a valid connection
                connections.append((
                    start_shape.id,
                    end_shape.id,
                    connector.text
                ))

                self.logger.debug(
                    f"Connection: '{start_shape.text}' → '{end_shape.text}' "
                    f"(label: '{connector.text}')"
                )

        self.logger.info(f"Inferred {len(connections)} connections from {len(connectors)} connectors")
        return connections

    def _find_closest_shape(
        self,
        position: Tuple[int, int],
        shapes: List[ShapeInfo],
        max_distance: int
    ) -> Optional[ShapeInfo]:
        """
        Find the shape closest to a given position.

        Args:
            position: (x, y) position
            shapes: List of shapes to search
            max_distance: Maximum distance to consider

        Returns:
            Closest shape or None if no shape within max_distance
        """
        min_distance = float('inf')
        closest_shape = None

        px, py = position

        for shape in shapes:
            # Calculate distance from position to shape center
            sx, sy = shape.center
            distance = math.sqrt((px - sx) ** 2 + (py - sy) ** 2)

            if distance < min_distance and distance <= max_distance:
                min_distance = distance
                closest_shape = shape

        return closest_shape


# Convenience function
def parse_connectors_from_xlsx(xlsx_path: str) -> Tuple[List[ShapeInfo], List[ConnectorInfo], List[Tuple[str, str, str]]]:
    """
    Parse shapes, connectors, and infer connections from an Excel file.

    Args:
        xlsx_path: Path to .xlsx file

    Returns:
        Tuple of (shapes, connectors, connections)
        where connections is a list of (from_id, to_id, label) tuples
    """
    parser = ConnectorParser()
    shapes, connectors = parser.parse_from_xlsx(xlsx_path)
    connections = parser.find_connected_shapes(shapes, connectors)
    return shapes, connectors, connections
