"""GraphQL schema for Geo service."""
import logging
import requests
import graphene
from django.conf import settings
from .arango_client import get_db, ensure_graph

logger = logging.getLogger(__name__)


class EdgeType(graphene.ObjectType):
    """Edge between two nodes (route)."""
    to_uuid = graphene.String()
    to_name = graphene.String()
    to_latitude = graphene.Float()
    to_longitude = graphene.Float()
    distance_km = graphene.Float()
    travel_time_seconds = graphene.Int()
    transport_type = graphene.String()


class NodeType(graphene.ObjectType):
    """Logistics node with edges to neighbors."""
    uuid = graphene.String()
    name = graphene.String()
    latitude = graphene.Float()
    longitude = graphene.Float()
    country = graphene.String()
    country_code = graphene.String()
    synced_at = graphene.String()
    transport_types = graphene.List(graphene.String)
    edges = graphene.List(EdgeType)


class NodeConnectionsType(graphene.ObjectType):
    """Auto + rail edges for a node, rail uses nearest rail node."""
    hub = graphene.Field(NodeType)
    rail_node = graphene.Field(NodeType)
    auto_edges = graphene.List(EdgeType)
    rail_edges = graphene.List(EdgeType)


class RouteType(graphene.ObjectType):
    """Route between two points with geometry."""
    distance_km = graphene.Float()
    geometry = graphene.JSONString(description="GeoJSON LineString coordinates")


class RouteStageType(graphene.ObjectType):
    """Single stage in a multi-hop route."""
    from_uuid = graphene.String()
    from_name = graphene.String()
    from_lat = graphene.Float()
    from_lon = graphene.Float()
    to_uuid = graphene.String()
    to_name = graphene.String()
    to_lat = graphene.Float()
    to_lon = graphene.Float()
    distance_km = graphene.Float()
    travel_time_seconds = graphene.Int()
    transport_type = graphene.String()


class RoutePathType(graphene.ObjectType):
    """Complete route through graph with multiple stages."""
    total_distance_km = graphene.Float()
    total_time_seconds = graphene.Int()
    stages = graphene.List(RouteStageType)


class ProductRouteOptionType(graphene.ObjectType):
    """Route options for a product source to the destination."""
    source_uuid = graphene.String()
    source_name = graphene.String()
    source_lat = graphene.Float()
    source_lon = graphene.Float()
    distance_km = graphene.Float()
    routes = graphene.List(RoutePathType)


class Query(graphene.ObjectType):
    """Root query."""

    node = graphene.Field(
        NodeType,
        uuid=graphene.String(required=True),
        description="Get node by UUID with all edges to neighbors",
    )

    nodes = graphene.List(
        NodeType,
        description="Get all nodes (without edges for performance)",
        limit=graphene.Int(),
        offset=graphene.Int(),
        transport_type=graphene.String(),
    )
    nodes_count = graphene.Int(
        transport_type=graphene.String(),
        description="Get total count of nodes (with optional transport filter)",
    )

    node_connections = graphene.Field(
        NodeConnectionsType,
        uuid=graphene.String(required=True),
        limit_auto=graphene.Int(default_value=12),
        limit_rail=graphene.Int(default_value=12),
        description="Get auto + rail edges for a node (rail uses nearest rail node)",
    )

    auto_route = graphene.Field(
        RouteType,
        from_lat=graphene.Float(required=True),
        from_lon=graphene.Float(required=True),
        to_lat=graphene.Float(required=True),
        to_lon=graphene.Float(required=True),
        description="Get auto route between two points via GraphHopper",
    )

    rail_route = graphene.Field(
        RouteType,
        from_lat=graphene.Float(required=True),
        from_lon=graphene.Float(required=True),
        to_lat=graphene.Float(required=True),
        to_lon=graphene.Float(required=True),
        description="Get rail route between two points via OpenRailRouting",
    )

    find_routes = graphene.List(
        RoutePathType,
        from_uuid=graphene.String(required=True),
        to_uuid=graphene.String(required=True),
        limit=graphene.Int(default_value=3),
        description="Find K shortest routes through graph between two nodes",
    )

    find_product_routes = graphene.List(
        ProductRouteOptionType,
        product_uuid=graphene.String(required=True),
        to_uuid=graphene.String(required=True),
        limit_sources=graphene.Int(default_value=3),
        limit_routes=graphene.Int(default_value=3),
        description="Find routes from product offer nodes to destination",
    )

    @staticmethod
    def _build_routes(db, from_uuid, to_uuid, limit):
        """Shared helper to compute K shortest routes between two nodes."""
        aql = """
        FOR path IN ANY K_SHORTEST_PATHS
            @from_vertex TO @to_vertex
            GRAPH 'optovia_graph'
            OPTIONS { weightAttribute: 'distance_km' }
            LIMIT @limit
            RETURN {
                vertices: path.vertices,
                edges: path.edges,
                weight: path.weight
            }
        """

        try:
            cursor = db.aql.execute(
                aql,
                bind_vars={
                    'from_vertex': f'nodes/{from_uuid}',
                    'to_vertex': f'nodes/{to_uuid}',
                    'limit': limit,
                },
            )
            paths = list(cursor)
        except Exception as e:
            logger.error("K_SHORTEST_PATHS query failed: %s", e)
            return []

        if not paths:
            logger.info("No paths found from %s to %s", from_uuid, to_uuid)
            return []

        routes = []
        for path in paths:
            vertices = path.get('vertices', [])
            edges = path.get('edges', [])

            # Build vertex lookup by _id
            vertex_by_id = {v['_id']: v for v in vertices}

            stages = []
            for edge in edges:
                from_node = vertex_by_id.get(edge['_from'], {})
                to_node = vertex_by_id.get(edge['_to'], {})

                stages.append(RouteStageType(
                    from_uuid=from_node.get('_key'),
                    from_name=from_node.get('name'),
                    from_lat=from_node.get('latitude'),
                    from_lon=from_node.get('longitude'),
                    to_uuid=to_node.get('_key'),
                    to_name=to_node.get('name'),
                    to_lat=to_node.get('latitude'),
                    to_lon=to_node.get('longitude'),
                    distance_km=edge.get('distance_km'),
                    travel_time_seconds=edge.get('travel_time_seconds'),
                    transport_type=edge.get('transport_type'),
                ))

            total_time = sum(s.travel_time_seconds or 0 for s in stages)

            routes.append(RoutePathType(
                total_distance_km=path.get('weight'),
                total_time_seconds=total_time,
                stages=stages,
            ))

        return routes

    def resolve_node(self, info, uuid):
        """
        Get a single node with all its outgoing edges.

        Returns node info + list of edges to neighbors with distances.
        """
        db = get_db()

        # Get node
        nodes_col = db.collection('nodes')
        node = nodes_col.get(uuid)
        if not node:
            return None

        # Get all outgoing edges from this node
        edges_col = db.collection('edges')
        aql = """
        FOR edge IN edges
            FILTER edge._from == @from_id
            LET to_node = DOCUMENT(edge._to)
            RETURN {
                to_uuid: to_node._key,
                to_name: to_node.name,
                to_latitude: to_node.latitude,
                to_longitude: to_node.longitude,
                distance_km: edge.distance_km,
                travel_time_seconds: edge.travel_time_seconds,
                transport_type: edge.transport_type
            }
        """
        cursor = db.aql.execute(aql, bind_vars={'from_id': f"nodes/{uuid}"})
        edges = list(cursor)

        logger.info("Node %s has %d edges", uuid, len(edges))

        return NodeType(
            uuid=node['_key'],
            name=node.get('name'),
            latitude=node.get('latitude'),
            longitude=node.get('longitude'),
            country=node.get('country'),
            country_code=node.get('country_code'),
            synced_at=node.get('synced_at'),
            transport_types=node.get('transport_types') or [],
            edges=[EdgeType(**e) for e in edges],
        )

    def resolve_nodes(self, info, limit=None, offset=None, transport_type=None):
        """Get all logistics nodes (without edges for list view)."""
        db = get_db()

        # Only return logistics nodes (not buyer/seller addresses)
        aql = """
        FOR node IN nodes
            FILTER node.node_type == 'logistics' OR node.node_type == null
            LET types = node.transport_types != null ? node.transport_types : []
            FILTER @transport_type == null OR @transport_type IN types
            SORT node.name ASC
            LIMIT @offset, @limit
            RETURN node
        """
        cursor = db.aql.execute(
            aql,
            bind_vars={
                'transport_type': transport_type,
                'offset': 0 if offset is None else offset,
                'limit': 1000000 if limit is None else limit,
            },
        )

        nodes = []
        for node in cursor:
            nodes.append(NodeType(
                uuid=node['_key'],
                name=node.get('name'),
                latitude=node.get('latitude'),
                longitude=node.get('longitude'),
                country=node.get('country'),
                country_code=node.get('country_code'),
                synced_at=node.get('synced_at'),
                transport_types=node.get('transport_types') or [],
                edges=[],  # Don't load edges for list
            ))

        logger.info("Returning %d nodes", len(nodes))
        return nodes

    def resolve_nodes_count(self, info, transport_type=None):
        db = get_db()
        aql = """
        FOR node IN nodes
            FILTER node.node_type == 'logistics' OR node.node_type == null
            LET types = node.transport_types != null ? node.transport_types : []
            FILTER @transport_type == null OR @transport_type IN types
            COLLECT WITH COUNT INTO length
            RETURN length
        """
        cursor = db.aql.execute(aql, bind_vars={'transport_type': transport_type})
        return next(cursor, 0)

    def resolve_node_connections(self, info, uuid, limit_auto=12, limit_rail=12):
        """Get auto edges from hub and rail edges from nearest rail node."""
        db = get_db()

        nodes_col = db.collection('nodes')
        hub = nodes_col.get(uuid)
        if not hub:
            return None

        aql = """
        LET auto_edges = (
            FOR edge IN edges
                FILTER edge._from == @from_id AND edge.transport_type == "auto"
                LET to_node = DOCUMENT(edge._to)
                FILTER to_node != null
                SORT edge.distance_km ASC
                LIMIT @limit_auto
                RETURN {
                    to_uuid: to_node._key,
                    to_name: to_node.name,
                    to_latitude: to_node.latitude,
                    to_longitude: to_node.longitude,
                    distance_km: edge.distance_km,
                    travel_time_seconds: edge.travel_time_seconds,
                    transport_type: edge.transport_type
                }
        )

        LET hub_has_rail = @hub_has_rail

        LET rail_node = hub_has_rail ? DOCUMENT(@from_id) : FIRST(
            FOR node IN nodes
                FILTER node.latitude != null AND node.longitude != null
                FILTER 'rail' IN node.transport_types
                SORT DISTANCE(@hub_lat, @hub_lon, node.latitude, node.longitude)
                LIMIT 1
                RETURN node
        )

        LET rail_edges = rail_node == null ? [] : (
            FOR edge IN edges
                FILTER edge._from == CONCAT("nodes/", rail_node._key) AND edge.transport_type == "rail"
                LET to_node = DOCUMENT(edge._to)
                FILTER to_node != null
                SORT edge.distance_km ASC
                LIMIT @limit_rail
                RETURN {
                    to_uuid: to_node._key,
                    to_name: to_node.name,
                    to_latitude: to_node.latitude,
                    to_longitude: to_node.longitude,
                    distance_km: edge.distance_km,
                    travel_time_seconds: edge.travel_time_seconds,
                    transport_type: edge.transport_type
                }
        )

        RETURN {
            hub: DOCUMENT(@from_id),
            rail_node: rail_node,
            auto_edges: auto_edges,
            rail_edges: rail_edges
        }
        """

        cursor = db.aql.execute(
            aql,
            bind_vars={
                'from_id': f"nodes/{uuid}",
                'hub_lat': hub.get('latitude'),
                'hub_lon': hub.get('longitude'),
                'hub_has_rail': 'rail' in (hub.get('transport_types') or []),
                'limit_auto': limit_auto,
                'limit_rail': limit_rail,
            },
        )
        result = next(cursor, None)
        if not result:
            return None

        def build_node(doc):
            if not doc:
                return None
            return NodeType(
                uuid=doc['_key'],
                name=doc.get('name'),
                latitude=doc.get('latitude'),
                longitude=doc.get('longitude'),
                country=doc.get('country'),
                country_code=doc.get('country_code'),
                synced_at=doc.get('synced_at'),
                transport_types=doc.get('transport_types') or [],
                edges=[],
            )

        return NodeConnectionsType(
            hub=build_node(result.get('hub')),
            rail_node=build_node(result.get('rail_node')),
            auto_edges=[EdgeType(**e) for e in result.get('auto_edges') or []],
            rail_edges=[EdgeType(**e) for e in result.get('rail_edges') or []],
        )

    def resolve_auto_route(self, info, from_lat, from_lon, to_lat, to_lon):
        """Get auto route via GraphHopper."""
        url = f"{settings.GRAPHHOPPER_EXTERNAL_URL}/route"
        params = {
            'point': [f"{from_lat},{from_lon}", f"{to_lat},{to_lon}"],
            'profile': 'car',
            'instructions': 'false',
            'calc_points': 'true',
            'points_encoded': 'false',
        }

        try:
            response = requests.get(url, params=params, timeout=30)
            response.raise_for_status()
            data = response.json()

            if 'paths' in data and len(data['paths']) > 0:
                path = data['paths'][0]
                distance_km = round(path.get('distance', 0) / 1000, 2)
                points = path.get('points', {})
                coordinates = points.get('coordinates', [])

                return RouteType(
                    distance_km=distance_km,
                    geometry=coordinates,
                )
        except requests.RequestException as e:
            logger.error("GraphHopper request failed: %s", e)

        return None

    def resolve_rail_route(self, info, from_lat, from_lon, to_lat, to_lon):
        """Get rail route via OpenRailRouting."""
        url = f"{settings.OPENRAILROUTING_EXTERNAL_URL}/route"
        params = {
            'point': [f"{from_lat},{from_lon}", f"{to_lat},{to_lon}"],
            'profile': 'all_tracks',
            'calc_points': 'true',
            'points_encoded': 'false',
        }

        try:
            response = requests.get(url, params=params, timeout=60)
            response.raise_for_status()
            data = response.json()

            if 'paths' in data and len(data['paths']) > 0:
                path = data['paths'][0]
                distance_km = round(path.get('distance', 0) / 1000, 2)
                points = path.get('points', {})
                coordinates = points.get('coordinates', [])

                return RouteType(
                    distance_km=distance_km,
                    geometry=coordinates,
                )
        except requests.RequestException as e:
            logger.error("OpenRailRouting request failed: %s", e)

        return None

    def resolve_find_routes(self, info, from_uuid, to_uuid, limit=3):
        """Find K shortest routes through graph using ArangoDB K_SHORTEST_PATHS."""
        db = get_db()
        ensure_graph()
        return Query._build_routes(db, from_uuid, to_uuid, limit)

    def resolve_find_product_routes(self, info, product_uuid, to_uuid, limit_sources=3, limit_routes=3):
        """Find routes from product offer nodes to destination."""
        db = get_db()
        ensure_graph()

        # Load destination node for distance sorting
        nodes_col = db.collection('nodes')
        dest = nodes_col.get(to_uuid)
        if not dest:
            logger.info("Destination node %s not found", to_uuid)
            return []

        dest_lat = dest.get('latitude')
        dest_lon = dest.get('longitude')
        if dest_lat is None or dest_lon is None:
            logger.info("Destination node %s missing coordinates", to_uuid)
            return []

        # Find offer nodes for the product, closest to destination
        aql = """
        FOR node IN nodes
            FILTER node.product_uuid == @product_uuid
            FILTER node.latitude != null AND node.longitude != null
            LET distance = DISTANCE(@dest_lat, @dest_lon, node.latitude, node.longitude)
            SORT distance ASC
            LIMIT @limit_sources
            RETURN {
                _key: node._key,
                name: node.name,
                latitude: node.latitude,
                longitude: node.longitude,
                distance_km: distance / 1000
            }
        """

        try:
            cursor = db.aql.execute(
                aql,
                bind_vars={
                    'product_uuid': product_uuid,
                    'dest_lat': dest_lat,
                    'dest_lon': dest_lon,
                    'limit_sources': limit_sources,
                },
            )
            sources = list(cursor)
        except Exception as e:
            logger.error("find_product_routes AQL failed: %s", e)
            return []

        if not sources:
            logger.info("No product sources found for %s", product_uuid)
            return []

        options = []
        for src in sources:
            src_uuid = src.get('_key')
            routes = Query._build_routes(db, src_uuid, to_uuid, limit_routes)

            options.append(ProductRouteOptionType(
                source_uuid=src_uuid,
                source_name=src.get('name'),
                source_lat=src.get('latitude'),
                source_lon=src.get('longitude'),
                distance_km=src.get('distance_km'),
                routes=routes,
            ))

        return options


schema = graphene.Schema(query=Query)
