import json
import csv
import io
import math
from typing import Dict, Any, List, Tuple
from xml.sax.saxutils import escape

from PIL import Image, ImageDraw, ImageFont

from config import NODE_COLORS

NODE_RADII = {
    'gateway': 28,
    'server': 22,
    'printer': 18,
    'workstation': 16,
    'unknown': 14,
}

BG_COLOR = '#f8f9fa'
EDGE_COLOR = '#cccccc'
LABEL_COLOR = '#333333'
SUBLABEL_COLOR = '#888888'


def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
    h = hex_color.lstrip('#')
    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))


class TopologyExporter:

    def export_json(self, topology_data: Dict[str, Any]) -> str:
        """Export topology data as JSON string."""
        export_data = {
            'metadata': {
                'export_format': 'network_topology_json',
                'version': '1.0',
                'total_nodes': len(topology_data.get('nodes', [])),
                'total_links': len(topology_data.get('links', [])),
                'node_types': topology_data.get('stats', {}).get('node_types', {}),
            },
            'topology': topology_data,
        }
        return json.dumps(export_data, indent=2, default=str)

    def export_csv_nodes(self, topology_data: Dict[str, Any]) -> str:
        """Export node data as CSV."""
        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(['IP', 'Hostname', 'Type', 'MAC_Address', 'Vendor', 'Open_Ports', 'State'])

        for node in topology_data.get('nodes', []):
            writer.writerow([
                node.get('ip', ''),
                node.get('hostname', ''),
                node.get('type', ''),
                node.get('mac_address', ''),
                node.get('vendor', ''),
                node.get('open_ports', 0),
                node.get('state', ''),
            ])

        buf.seek(0)
        return buf.getvalue()

    def export_csv_links(self, topology_data: Dict[str, Any]) -> str:
        """Export link data as CSV."""
        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(['Source', 'Target', 'Weight', 'Type'])

        for link in topology_data.get('links', []):
            writer.writerow([
                link.get('source', ''),
                link.get('target', ''),
                link.get('weight', 0),
                link.get('type', ''),
            ])

        buf.seek(0)
        return buf.getvalue()

    # ------------------------------------------------------------------ #
    #  Image export (PNG / JPEG / SVG)
    # ------------------------------------------------------------------ #

    def export_image(self, topology_data: Dict[str, Any], path: str,
                     width: int = 900, height: int = 700) -> None:
        """
        Export topology image. Format is detected from file extension.

        Supported: .png, .jpg/.jpeg, .svg
        """
        ext = path.rsplit('.', 1)[-1].lower() if '.' in path else ''

        if ext == 'svg':
            svg = self.export_svg(topology_data, width, height)
            with open(path, 'w', encoding='utf-8') as f:
                f.write(svg)
        elif ext in ('png', 'jpg', 'jpeg'):
            img_bytes = self._render_raster(topology_data, width, height, fmt=ext)
            with open(path, 'wb') as f:
                f.write(img_bytes)
        else:
            raise ValueError(
                f"Unsupported image format '.{ext}'. Use .png, .jpg, or .svg"
            )

    # ------------------------------------------------------------------ #
    #  Raster rendering (PNG / JPEG) via Pillow
    # ------------------------------------------------------------------ #

    def _render_raster(self, topology_data: Dict[str, Any],
                       width: int, height: int, fmt: str = 'png') -> bytes:
        """Render topology to PNG or JPEG bytes using Pillow."""
        nodes = topology_data.get('nodes', [])
        links = topology_data.get('links', [])

        img = Image.new('RGB', (width, height), _hex_to_rgb(BG_COLOR))
        draw = ImageDraw.Draw(img)

        try:
            font = ImageFont.truetype("arial.ttf", 12)
            font_small = ImageFont.truetype("arial.ttf", 10)
            font_title = ImageFont.truetype("arial.ttf", 18)
        except (OSError, IOError):
            font = ImageFont.load_default()
            font_small = font
            font_title = font

        if not nodes:
            draw.text(
                (width // 2, height // 2),
                "No hosts discovered",
                fill=_hex_to_rgb(SUBLABEL_COLOR),
                font=font_title,
                anchor='mm',
            )
            return self._image_to_bytes(img, fmt)

        positions = self._compute_layout(nodes, width, height)

        # Title
        stats = topology_data.get('stats', {})
        title = f"Network Topology — {stats.get('total_nodes', 0)} hosts"
        draw.text((width // 2, 25), title, fill=_hex_to_rgb(LABEL_COLOR),
                  font=font_title, anchor='mm')

        # Edges
        pos_map = {n['id']: positions[i] for i, n in enumerate(nodes)}
        edge_rgb = _hex_to_rgb(EDGE_COLOR)
        for link in links:
            src = pos_map.get(link.get('source'))
            tgt = pos_map.get(link.get('target'))
            if src and tgt:
                draw.line([src, tgt], fill=edge_rgb, width=2)

        # Nodes
        for i, node in enumerate(nodes):
            x, y = positions[i]
            ntype = node.get('type', 'unknown')
            color = _hex_to_rgb(NODE_COLORS.get(ntype, NODE_COLORS['unknown']))
            r = NODE_RADII.get(ntype, NODE_RADII['unknown'])

            draw.ellipse(
                [x - r, y - r, x + r, y + r],
                fill=color, outline=(255, 255, 255), width=2,
            )

            label = node.get('label') or node.get('ip', '')
            ip = node.get('ip', '')

            draw.text((x, y - r - 8), label, fill=_hex_to_rgb(LABEL_COLOR),
                      font=font, anchor='mb')
            if label != ip:
                draw.text((x, y - r - 22), ip, fill=_hex_to_rgb(SUBLABEL_COLOR),
                          font=font_small, anchor='mb')

        # Legend
        self._draw_legend(draw, width, font)

        return self._image_to_bytes(img, fmt)

    def _draw_legend(self, draw: ImageDraw.ImageDraw, width: int, font) -> None:
        items = [
            ('Gateway', NODE_COLORS['gateway']),
            ('Server', NODE_COLORS['server']),
            ('Workstation', NODE_COLORS['workstation']),
            ('Printer', NODE_COLORS['printer']),
        ]
        lx = width - 135
        ly = 50
        box_h = len(items) * 22 + 12

        draw.rounded_rectangle(
            [lx - 10, ly - 10, lx + 120, ly - 10 + box_h],
            radius=5, fill=(255, 255, 255), outline=(221, 221, 221),
        )

        for i, (label, color) in enumerate(items):
            cy = ly + i * 22
            r = 6
            draw.ellipse(
                [lx, cy - r, lx + 2 * r, cy + r],
                fill=_hex_to_rgb(color),
            )
            draw.text((lx + 18, cy), label, fill=(85, 85, 85), font=font, anchor='lm')

    @staticmethod
    def _image_to_bytes(img: Image.Image, fmt: str) -> bytes:
        buf = io.BytesIO()
        save_fmt = 'JPEG' if fmt in ('jpg', 'jpeg') else 'PNG'
        if save_fmt == 'JPEG':
            img = img.convert('RGB')
        img.save(buf, format=save_fmt, quality=95)
        buf.seek(0)
        return buf.getvalue()

    # ------------------------------------------------------------------ #
    #  SVG rendering (no Pillow needed)
    # ------------------------------------------------------------------ #

    def export_svg(self, topology_data: Dict[str, Any],
                   width: int = 900, height: int = 700) -> str:
        """Export topology as SVG string."""
        nodes = topology_data.get('nodes', [])
        links = topology_data.get('links', [])

        if not nodes:
            return self._empty_svg(width, height)

        positions = self._compute_layout(nodes, width, height)

        parts: List[str] = []
        parts.append(
            f'<svg xmlns="http://www.w3.org/2000/svg" '
            f'width="{width}" height="{height}" '
            f'viewBox="0 0 {width} {height}" '
            f'style="font-family:sans-serif;background:{BG_COLOR}">'
        )
        parts.append(self._svg_defs())

        stats = topology_data.get('stats', {})
        title = f"{stats.get('total_nodes', 0)} hosts"
        parts.append(
            f'<text x="{width // 2}" y="30" text-anchor="middle" '
            f'font-size="18" font-weight="bold" fill="{LABEL_COLOR}">'
            f'Network Topology — {escape(title)}</text>'
        )

        pos_map = {n['id']: positions[i] for i, n in enumerate(nodes)}
        for link in links:
            src = pos_map.get(link.get('source'))
            tgt = pos_map.get(link.get('target'))
            if src and tgt:
                parts.append(
                    f'<line x1="{src[0]}" y1="{src[1]}" '
                    f'x2="{tgt[0]}" y2="{tgt[1]}" '
                    f'stroke="{EDGE_COLOR}" stroke-width="1.5" />'
                )

        for i, node in enumerate(nodes):
            x, y = positions[i]
            ntype = node.get('type', 'unknown')
            color = NODE_COLORS.get(ntype, NODE_COLORS['unknown'])
            r = NODE_RADII.get(ntype, NODE_RADII['unknown'])
            label = escape(node.get('label') or node.get('ip', ''))
            ip = escape(node.get('ip', ''))

            parts.append(
                f'<circle cx="{x}" cy="{y}" r="{r}" '
                f'fill="{color}" stroke="#fff" stroke-width="2" />'
            )
            parts.append(
                f'<text x="{x}" y="{y - r - 6}" text-anchor="middle" '
                f'font-size="11" fill="{LABEL_COLOR}">{label}</text>'
            )
            if label != ip:
                parts.append(
                    f'<text x="{x}" y="{y - r - 18}" text-anchor="middle" '
                    f'font-size="9" fill="{SUBLABEL_COLOR}">{ip}</text>'
                )

        parts.append(self._svg_legend(width))
        parts.append('</svg>')
        return '\n'.join(parts)

    # ------------------------------------------------------------------ #
    #  Shared layout
    # ------------------------------------------------------------------ #

    def _compute_layout(self, nodes: List[Dict], width: int, height: int):
        """Star layout: gateway at center, others in a circle."""
        cx, cy = width // 2, height // 2
        radius = min(width, height) * 0.35

        gateways = [i for i, n in enumerate(nodes) if n.get('type') == 'gateway']
        others = [i for i in range(len(nodes)) if i not in gateways]

        positions = [(0.0, 0.0)] * len(nodes)

        if gateways:
            for gi in gateways:
                positions[gi] = (float(cx), float(cy))

        ring_nodes = others if gateways else list(range(len(nodes)))
        n_ring = len(ring_nodes)
        if n_ring == 0:
            return positions

        if not gateways and n_ring == 1:
            positions[ring_nodes[0]] = (float(cx), float(cy))
            return positions

        for idx, ni in enumerate(ring_nodes):
            angle = 2 * math.pi * idx / n_ring - math.pi / 2
            x = cx + radius * math.cos(angle)
            y = cy + radius * math.sin(angle)
            positions[ni] = (round(x, 1), round(y, 1))

        return positions

    # ------------------------------------------------------------------ #
    #  SVG helpers
    # ------------------------------------------------------------------ #

    def _svg_defs(self) -> str:
        return (
            '<defs><filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">'
            '<feDropShadow dx="1" dy="1" stdDeviation="2" flood-opacity="0.15" />'
            '</filter></defs>'
        )

    def _svg_legend(self, width: int) -> str:
        items = [
            ('Gateway', NODE_COLORS['gateway']),
            ('Server', NODE_COLORS['server']),
            ('Workstation', NODE_COLORS['workstation']),
            ('Printer', NODE_COLORS['printer']),
        ]
        parts = []
        lx = width - 130
        ly = 55
        parts.append(
            f'<rect x="{lx - 10}" y="{ly - 15}" width="130" height="{len(items) * 22 + 10}" '
            f'rx="5" fill="white" stroke="#ddd" />'
        )
        for i, (label, color) in enumerate(items):
            y = ly + i * 22
            parts.append(f'<circle cx="{lx + 6}" cy="{y}" r="6" fill="{color}" />')
            parts.append(
                f'<text x="{lx + 18}" y="{y + 4}" font-size="11" fill="#555">{label}</text>'
            )
        return '\n'.join(parts)

    def _empty_svg(self, width: int, height: int) -> str:
        return (
            f'<svg xmlns="http://www.w3.org/2000/svg" '
            f'width="{width}" height="{height}" style="background:{BG_COLOR}">'
            f'<text x="{width // 2}" y="{height // 2}" text-anchor="middle" '
            f'font-size="16" fill="#999">No hosts discovered</text></svg>'
        )

    def get_export_formats(self) -> List[str]:
        """Get list of available export formats."""
        return ['json', 'png', 'jpg', 'svg', 'csv_nodes', 'csv_links']
