import argparse

import casageo.client
import casageo.client.types
import geopandas as gpd
import pandas as pd
from casageo.client.api.v1 import (
    v1_isolines_create,
    v1_poi_retrieve,
    v1_revgeocode_retrieve,
    v1_routes_create,
)
from casageo.client.models.v1_isolines_create_rangetype import V1IsolinesCreateRangetype
from casageo.client.models.v1_isolines_create_routing_mode import (
    V1IsolinesCreateRoutingMode,
)
from casageo.client.models.v1_isolines_create_transport_mode import (
    V1IsolinesCreateTransportMode,
)
from casageo.client.models.v1_routes_create_routing_mode import (
    V1RoutesCreateRoutingMode,
)
from casageo.client.models.v1_routes_create_transport_mode import (
    V1RoutesCreateTransportMode,
)
from shapely.geometry import MultiLineString, MultiPolygon, Point, Polygon

import casageo._parsers as parsers
import casageo._util as util
import casageo.account
import casageo.consts
from casageo._util import load_json, implicit_enum_parameters, point_to_string
from casageo.types import CasaGeoError


class CasaGeoSpatial:
    def __init__(self, key: str, lang: str | None = None):
        self.lang = lang
        self.client = casageo.client.AuthenticatedClient(
            casageo.consts.SERVER, key, prefix=""
        )

    @implicit_enum_parameters
    def isolines_json(
            self,
            *,
            range_values: list[float],
            point: Point,
            range_type: V1IsolinesCreateRangetype = V1IsolinesCreateRangetype.TIME,
            transport_mode: V1IsolinesCreateTransportMode = V1IsolinesCreateTransportMode.CAR,
            routing_mode: V1IsolinesCreateRoutingMode = V1IsolinesCreateRoutingMode.FAST,
    ) -> dict:
        """
        Response Structure: https://www.here.com/docs/bundle/isoline-routing-api-developer-guide-v8/page/topics/isoline-response.html
        """

        if range_type == V1IsolinesCreateRangetype.TIME:
            range_values = map(lambda x: x * 60, range_values) # We want minutes
        range_values = map(int, range_values)

        response = v1_isolines_create.sync_detailed(
            client=self.client,
            origin=util.point_to_string(point),
            routing_mode=routing_mode,
            transport_mode=transport_mode,
            rangetype=range_type,
            rangevalues=",".join(map(str, range_values)),
        )
        data = load_json(
            response
        )

        for isoline in data["isolines"]:
            for polygon in isoline["polygons"]:
                polygon["outer"] = util.load_flexpolyline(polygon["outer"])

        return data

    def isolines(
            self,
            *,
            range_values: list[float],
            point: Point,
            range_type: V1IsolinesCreateRangetype = V1IsolinesCreateRangetype.TIME,
            transport_mode: V1IsolinesCreateTransportMode = V1IsolinesCreateTransportMode.CAR,
            routing_mode: V1IsolinesCreateRoutingMode = V1IsolinesCreateRoutingMode.FAST,
    ) -> gpd.GeoDataFrame:


        data = self.isolines_json(
            range_values=range_values,
            point=point,
            range_type=range_type,
            transport_mode=transport_mode,
            routing_mode=routing_mode,
        )
        return gpd.GeoDataFrame(
            {
                "geometry": [
                    MultiPolygon(
                        Polygon(polygon["outer"])
                        for polygon in isoline["polygons"]
                    )
                    for isoline in data["isolines"]
                ],
                "names": [
                    f"{isoline['range']['type']}: {isoline['range']['value']}"
                    for isoline in data["isolines"]
                ],
            }, crs="EPSG:4326"
        )

    @implicit_enum_parameters
    def routes_json(
            self,
            *,
            return_: str,
            a: Point,
            b: Point,
            routing_mode: V1RoutesCreateRoutingMode,
            transport_mode: V1RoutesCreateTransportMode,
    ) -> dict:
        data = load_json(
            v1_routes_create.sync_detailed(
                return_=return_,
                client=self.client,
                origin=point_to_string(a),
                destination=point_to_string(b),
                routing_mode=routing_mode,
                transport_mode=transport_mode,
            )
        )

        for route in data["routes"]:
            for section in route["sections"]:
                if "polyline" in section:
                    section["polyline"] = util.load_flexpolyline(
                        section["polyline"]
                    )

        return data

    def routes(
            self,
            *,
            return_: str,
            a: Point,
            b: Point,
            routing_mode: V1RoutesCreateRoutingMode,
            transport_mode: V1RoutesCreateTransportMode,
    ) -> gpd.GeoDataFrame:
        data = self.routes_json(
            return_=return_,
            a=a,
            b=b,
            routing_mode=routing_mode,
            transport_mode=transport_mode,
        )
        geometry = [
            MultiLineString(
                [
                    section["polyline"]
                    for section in route["sections"]
                    if "polyline" in section
                ]
            )
            for route in data["routes"]
        ]

        return gpd.GeoDataFrame(geometry=geometry, crs="EPSG:4326")

    def poi_json(
            self,
            point: Point,
            limit: int,
    ) -> dict:
        return load_json(
            v1_poi_retrieve.sync_detailed(
                client=self.client,
                at=point_to_string(point),
                limit=limit,
            )
        )

    def poi(
            self,
            point: Point,
            limit: int,
    ) -> gpd.GeoDataFrame:
        data = self.poi_json(point, limit)
        return gpd.GeoDataFrame(
            {
                "geometry": [
                    Point(item["position"]["lng"], item["position"]["lat"])
                    for item in data["items"]
                ],
                "names": [item["title"] for item in data["items"]],
            }
            , crs="EPSG:4326")

    def revgeocode_json(self, point: Point) -> dict:
        return load_json(
            v1_revgeocode_retrieve.sync_detailed(
                client=self.client,
                at=point_to_string(point),
                lang=(self.lang if self.lang else casageo.client.types.UNSET),
            )
        )

    def revgeocode_str(self, point: Point) -> str:
        return self.revgeocode_json(point)["items"][0]["title"]

    def revgeocode(self, point: Point) -> gpd.GeoDataFrame:
        data = self.revgeocode_json(point)
        return gpd.GeoDataFrame(
            {
                "geometry": [
                    Point(item["position"]["lng"], item["position"]["lat"])
                    for item in data["items"]
                ],
                "names": [item["title"] for item in data["items"]],
            }, crs="EPSG:4326"
        )


def _main(args: argparse.Namespace) -> dict | pd.DataFrame:
    cgs = CasaGeoSpatial(args.key, args.lang)
    match args.command:
        case "isolines":
            p = util.string_to_point(args.point)
            range_values = list(map(float, args.range.split(",")))

            if args.raw:
                return cgs.isolines_json(
                    range_values=range_values,
                    point=p,
                    range_type=V1IsolinesCreateRangetype(args.rangetype),
                    transport_mode=V1IsolinesCreateTransportMode(args.vehicle),
                    routing_mode=V1IsolinesCreateRoutingMode(args.routing_mode),
                )
            else:
                return cgs.isolines(
                    range_values=range_values,
                    point=p,
                    range_type=V1IsolinesCreateRangetype(args.rangetype),
                    transport_mode=V1IsolinesCreateTransportMode(args.vehicle),
                    routing_mode=V1IsolinesCreateRoutingMode(args.routing_mode),
                )
        case "routes":
            a = util.string_to_point(args.pointa)
            b = util.string_to_point(args.pointb)
            if args.raw:
                return cgs.routes_json(
                    return_=vars(args)["return"],  # return is a keyword we can't do args.return
                    a=a,
                    b=b,
                    routing_mode=V1RoutesCreateRoutingMode(args.routing_mode),
                    transport_mode=V1RoutesCreateTransportMode(args.transport_mode),
                )
            else:
                return cgs.routes(
                    return_=vars(args)["return"],
                    a=a,
                    b=b,
                    routing_mode=V1RoutesCreateRoutingMode(args.routing_mode),
                    transport_mode=V1RoutesCreateTransportMode(args.transport_mode),
                )
        case "poi":
            p = util.string_to_point(args.point)
            if args.raw:
                return cgs.poi_json(p, limit=args.limit)
            else:
                return cgs.poi(
                    p, limit=args.limit
                )
        case "revgeocode":
            p = util.string_to_point(args.at)
            if args.raw:
                return cgs.revgeocode_json(p)
            else:
                return cgs.revgeocode(p)
        case "get-info":
            return casageo.account.account_info(args.key)
        case _:
            raise CasaGeoError(f"Unknown command: {args.command}")


if __name__ == "__main__":
    util.cli_main(parsers.get_spatial_parser(), _main)
