import contextlib
from collections import ChainMap
from collections.abc import Generator, Mapping, Sequence

import httpx
import pandas as pd
from shapely import Point

from casageo.tools import CasaGeoClient, CasaGeoError, _consts, _util


def T(func, *args, **kwargs):
    return _util.Transducer(_util.optional(func), *args, **kwargs)


def _delna(d: dict) -> dict:
    return {k: v for k, v in d.items() if pd.notna(v)}


def _records(df: pd.DataFrame, defaults: dict | None = None) -> Generator[Mapping]:
    records = df.to_dict(orient="records")
    defaults = defaults or {}
    return (ChainMap(_delna(r), defaults) for r in records)


def _getpoint(q: Mapping, key: str) -> Point | None:
    with contextlib.suppress(KeyError):
        return Point(q[key])
    with contextlib.suppress(KeyError):
        return Point(q[f"{key}_longitude"], q[f"{key}_latitude"], q[f"{key}_elevation"])
    with contextlib.suppress(KeyError):
        return Point(q[f"{key}_longitude"], q[f"{key}_latitude"])
    return None


def _coords(p: Point) -> tuple[float, float]:
    return (p.x, p.y)


def _split_if_str(s, *args, **kwargs):
    return s.split(*args, **kwargs) if isinstance(s, str) else s


def _raise_for_status(response: httpx.Response) -> None:
    if response.status_code != httpx.codes.OK:
        # FIXME: Proper error handling.
        msg = f"Bad status code {response.status_code}: {response.content!r}"
        raise CasaGeoError(msg)


def _address_getpos(q: Mapping) -> Point | None:
    # FIXME: Decide the name of this parameter.
    if p := _getpoint(q, "position") is not None:
        return p
    if p := _getpoint(q, "context_position") is not None:
        return p
    return None


def address(
    client: httpx.Client,
    queries: pd.DataFrame,
    defaults: dict | None = None,
    *,
    address_details: bool = False,
    coordinates: bool = False,
    match_quality: bool = False,
):
    method = "POST"
    url = "/api/v2/address"
    json = {
        "options": {
            "address_details": address_details,
            "coordinates": coordinates,
            "match_quality": match_quality,
        },
        "queries": [
            _delna({
                "address": q.get("address"),
                "country": q.get("country"),
                "state": q.get("state"),
                "county": q.get("county"),
                "city": q.get("city"),
                "district": q.get("district"),
                "street": q.get("street"),
                "housenumber": q.get("housenumber"),
                "postalcode": q.get("postalcode"),
                # Coder params
                "language": q.get("language"),
                "political_view": q.get("political_view"),
                "position": _address_getpos(q) | T(_coords),
                "limit": q.get("limit"),
                "countries": q.get("countries") | T(_split_if_str, ","),
                "address_names_mode": q.get("address_names_mode"),
                "postal_code_mode": q.get("postal_code_mode"),
            })
            for q in _records(queries, defaults)
        ],
    }

    response = client.request(method=method, url=url, json=json)
    _raise_for_status(response)
    return response.json()
    # response_json = response.json()
    # rid_series = queries.get("id", queries.index)
    # for rid, result in zip(rid_series, response_json["results"], strict=True):
    #     AddressRes
    #     pass


def poi(
    client: httpx.Client,
    queries: pd.DataFrame,
    defaults: dict | None = None,
    *,
    address_details: bool = False,
    coordinates: bool = False,
    category_codes: bool = False,
):
    method = "POST"
    url = "/api/v2/poi"
    json = {
        "options": {
            "address_details": address_details,
            "coordinates": coordinates,
            "category_codes": category_codes,
        },
        "queries": [
            _delna({
                "position": _getpoint(q, "position") | T(_coords),
                "categories": q.get("categories") | T(_split_if_str, ","),
                "chains": q.get("chains") | T(_split_if_str, ","),
                "food_types": q.get("food_types") | T(_split_if_str, ","),
                # Coder params
                "language": q.get("language"),
                "political_view": q.get("political_view"),
                "limit": q.get("limit"),
                "countries": q.get("countries") | T(_split_if_str, ","),
                "address_names_mode": q.get("address_names_mode"),
                "postal_code_mode": q.get("postal_code_mode"),
            })
            for q in _records(queries, defaults)
        ],
    }

    response = client.request(method=method, url=url, json=json)
    _raise_for_status(response)
    return response.json()


def _isolines_ranges_unit(q: Mapping) -> str | None:
    if u := q.get("ranges_unit") is not None:
        return u

    match q.get("range_type"):
        case None:
            pass
        case "time":
            return "minutes"
        case "distance":
            return "meters"
        case t:
            return t

    return None


def isolines(
    client: httpx.Client,
    queries: pd.DataFrame,
    defaults: dict | None = None,
    *,
    departure_info: bool = False,
    arrival_info: bool = False,
):
    method = "POST"
    url = "/api/v2/isolines"
    json = {
        "options": {
            "departure_info": departure_info,
            "arrival_info": arrival_info,
        },
        "queries": [
            _delna({
                "position": _getpoint(q, "position") | T(_coords),
                "ranges": q.get("ranges") | T(_split_if_str, ","),
                "ranges_unit": _isolines_ranges_unit(q),
                # Spatial params
                "language": q.get("language"),
                "unit_system": q.get("unit_system"),
                "transport_mode": q.get("transport_mode"),
                "routing_mode": q.get("routing_mode"),
                "direction": q.get("direction"),
                "departure_time": q.get("departure_time"),
                "arrival_time": q.get("arrival_time"),
                "traffic": q.get("traffic"),
                "avoid_features": q.get("avoid_features") | T(_split_if_str, ","),
                "exclude_countries": q.get("exclude_countries") | T(_split_if_str, ","),
            })
            for q in _records(queries, defaults)
        ],
    }

    response = client.request(method=method, url=url, json=json)
    _raise_for_status(response)
    return response.json()


def routes(
    client: httpx.Client,
    queries: pd.DataFrame,
    defaults: dict | None = None,
    *,
    departure_info: bool = False,
    arrival_info: bool = False,
):
    method = "POST"
    url = "/api/v2/routes"
    json = {
        "options": {
            "departure_info": departure_info,
            "arrival_info": arrival_info,
        },
        "queries": [
            _delna({
                "origin": _getpoint(q, "origin") | T(_coords),
                "destination": _getpoint(q, "destination") | T(_coords),
                "alternatives": q.get("alternatives"),
                # Spatial params
                "language": q.get("language"),
                "unit_system": q.get("unit_system"),
                "transport_mode": q.get("transport_mode"),
                "routing_mode": q.get("routing_mode"),
                "direction": q.get("direction"),
                "departure_time": q.get("departure_time"),
                "arrival_time": q.get("arrival_time"),
                "traffic": q.get("traffic"),
                "avoid_features": q.get("avoid_features") | T(_split_if_str, ","),
                "exclude_countries": q.get("exclude_countries") | T(_split_if_str, ","),
            })
            for q in _records(queries, defaults)
        ],
    }

    response = client.request(method=method, url=url, json=json)
    _raise_for_status(response)
    return response.json()


def _main(args: Sequence[str] | None = None) -> None:
    import argparse
    import json
    import os
    import sys

    parser = argparse.ArgumentParser(allow_abbrev=False)
    subparsers = parser.add_subparsers(required=True, metavar="COMMAND")

    address_parser = subparsers.add_parser("address")
    address_parser.set_defaults(command=address)
    address_parser.add_argument(
        "queries",
        nargs="?",
        default=sys.stdin,
        type=argparse.FileType("r"),
    )

    poi_parser = subparsers.add_parser("poi")
    poi_parser.set_defaults(command=poi)
    poi_parser.add_argument(
        "queries",
        nargs="?",
        default=sys.stdin,
        type=argparse.FileType("r"),
    )

    isolines_parser = subparsers.add_parser("isolines")
    isolines_parser.set_defaults(command=isolines)
    isolines_parser.add_argument(
        "queries",
        nargs="?",
        default=sys.stdin,
        type=argparse.FileType("r"),
    )

    routes_parser = subparsers.add_parser("routes")
    routes_parser.set_defaults(command=routes)
    routes_parser.add_argument(
        "queries",
        nargs="?",
        default=sys.stdin,
        type=argparse.FileType("r"),
    )

    options = parser.parse_args(args)
    api_key = os.getenv("CASAGEOTOOLS_API_KEY") or sys.exit(
        "Please specify an API key in the environment variable CASAGEOTOOLS_API_KEY"
    )
    base_url = os.getenv("CASAGEOTOOLS_PROXY_SERVER") or _consts.SERVER

    client = CasaGeoClient(api_key)._client.get_httpx_client()
    client.base_url = base_url
    result = options.command(
        client,
        pd.read_json(
            options.queries,
            orient="records",
        ),
    )

    json.dump(result, sys.stdout, ensure_ascii=False, indent=4, sort_keys=True)
    sys.stdout.write("\n")


if __name__ == "__main__":
    _main()
