"""Casageocoder is responsible for encoding and decoding of addresses."""
import argparse

import casageo.client
import pandas as pd
from casageo.client.api.v1 import v1_geocoder_create

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


def _get_dict(d: dict, loc: str):
    for key in loc.split("."):
        if not d or key not in d:
            return None
        d = d[key]
    return d


def _addr_fields_to_str(
        *,
        country: str = "",
        state: str = "",
        county: str = "",
        city: str = "",
        district: str = "",
        street: str = "",
        housenumber: str = "",
        postalcode: str = "",
        address_string: str = "",
        id: str = "",  # this is not relevant for the address but is needed to make the bulk geocoder happy
) -> str:
    if address_string:
        return address_string
    return f"{street} {housenumber}, {district}, {postalcode} {city}, {county}, {state}, {country}"


def _ensure_float(n: int | float | None) -> float | None:
    if isinstance(n, int):
        return float(n)
    return n


def _get_first(n: list | None):
    if isinstance(n, list):
        return n[0]
    return n


def _dict2df(data: dict):
    """
    Data is a dict of the form `{ index1 : { index2 : here_response }}`.
    """
    out = []
    for i1, v1 in data.items():
        for i2, v in enumerate(v1["items"]):
            out.append(
                {
                    "id": i1,
                    "subid": i2,
                    "inputStr": v1.get("inputStr"),
                    "label": _get_dict(v, "address.label"),
                    "street": _get_dict(v, "address.street"),
                    "houseNumber": _get_dict(v, "address.houseNumber"),
                    "postalCode": _get_dict(v, "address.postalCode"),
                    "city": _get_dict(v, "address.city"),
                    "county": _get_dict(v, "address.county"),
                    "state": _get_dict(v, "address.state"),
                    "stateCode": _get_dict(v, "address.stateCode"),
                    "countryName": _get_dict(v, "address.countryName"),
                    "access_lat": _ensure_float(
                        _get_dict(_get_first(v.get("access")), "lat")
                    ),
                    "access_lng": _ensure_float(
                        _get_dict(_get_first(v.get("access")), "lng")
                    ),
                    "pos_lat": _ensure_float(_get_dict(v, "position.lat")),
                    "pos_lng": _ensure_float(_get_dict(v, "position.lng")),
                    "resultType": v.get("resultType"),
                    "houseNumberType": v.get("houseNumberType"),
                    "countryCode": _get_dict(v, "address.countryCode"),
                    "countyCode": _get_dict(v, "address.countyCode"),
                    "scoreQuery": _ensure_float(_get_dict(v, "scoring.queryScore")),
                    "score_country": _ensure_float(
                        _get_dict(v, "scoring.fieldScore.country")
                    ),
                    "score_city": _ensure_float(
                        _get_dict(v, "scoring.fieldScore.city")
                    ),
                    "score_houseNumber": _ensure_float(
                        _get_dict(v, "scoring.fieldScore.houseNumber")
                    ),
                    "score_postalCode": _ensure_float(
                        _get_dict(v, "scoring.fieldScore.postalCode")
                    ),
                    "score_streets": _ensure_float(
                        _get_first(_get_dict(v, "scoring.fieldScore.streets"))
                    ),
                    # "score_streets": Maybe(v)["scoring"]["fieldScore"]["streets"][0].map(float).get(),
                }
            )
    df = pd.DataFrame(out)
    df = df.reindex(columns=casageo.consts.TABLE_ORDER)
    return df


class CasaGeoCoder:
    def __init__(self, key: str, lang: str | None = None) -> None:
        self.lang = lang
        self.client = casageo.client.AuthenticatedClient(
            base_url=casageo.consts.SERVER, token=key, prefix=""
        )

    def _request_str_json(self, addr: str) -> dict:
        response = load_json(v1_geocoder_create.sync_detailed(client=self.client, q=addr))
        response["inputStr"] = addr
        return response

    def address_json(self, address: str | dict) -> dict:
        """
        Geocode an address and return the API response as a dict.

        For the result structure see
        https://www.here.com/docs/bundle/geocoding-and-search-api-developer-guide/page/topics-api/code-geocode-address.html.
        """
        if isinstance(address, dict):
            # TODO: This should use a qualified query instead.
            address = _addr_fields_to_str(**address)
        return self._request_str_json(address)

    def address(self, address: str | dict) -> pd.DataFrame:
        """Geocode an address and return the API response as a dataframe."""
        return _dict2df({"command": self.address_json(address)})

    def bulk_address_json(self, address_df: pd.DataFrame) -> dict:
        """
        Geocode all addresses in the input dataframe and return a dict.

        Warning: If a request errors it may drop the previous data.
        """
        if account.account_info(self.client.token)["credits"] < len(address_df) * casageo.consts.GEOCODE_COST:
            raise CasaGeoError(
                f"Insufficient credits to geocode {len(address_df)} addresses"
            )
        out = {}
        for i, row in address_df.iterrows():
            out[i] = self.address_json(row.to_dict())
            out[i]["id"] = row["id"]
        return out

    def bulk_address(self, address_df: pd.DataFrame) -> pd.DataFrame:
        """
        Geocode all addresses in the input dataframe and return a dataframe.

        Warning: If a request errors it may drop the previous data.
        """
        return _dict2df(self.bulk_address_json(address_df))


def _main(args: argparse.Namespace) -> dict | pd.DataFrame:
    cgc = CasaGeoCoder(args.key, lang=args.lang)

    if args.get_info:
        return account.account_info(args.key)
    elif args.addrstr:
        if args.json:
            return cgc.address_json(args.addrstr)
        else:
            return cgc.address(args.addrstr)
    elif args.file:
        df = pd.read_csv(args.file, sep=";")
        if args.format:
            column_format = args.format.split(",")
            if len(column_format) != len(df.columns):
                raise CasaGeoError(
                    "The amount of columns in format and data must be equal"
                )

            if not set(column_format).issubset(casageo.consts.ALLOWED_COLUMNS):
                raise CasaGeoError(
                    f"The format must only contain {', '.join(casageo.consts.ALLOWED_COLUMNS)}."
                )

            dupes = util.duplicates(column_format)
            while "_" in dupes:
                dupes.remove("_")
            if dupes:
                raise CasaGeoError(
                    "There cannot be multiple columns of the same name except for _"
                )

            # if there is more than one _ rename them to _1, _2 etc
            temp_format = []
            for i, name in enumerate(column_format):
                if name == "_":
                    name += str(i)
                temp_format.append(name)
            column_format = temp_format

            df.columns = column_format

        if 'id' not in df.columns:
            raise CasaGeoError("DataFrame must contain an 'id' column")

        if df['id'].duplicated().any():
            raise CasaGeoError("'id' column contains duplicate values")

        if args.json:
            return cgc.bulk_address_json(df)
        else:
            return cgc.bulk_address(df)
    else:
        raise CasaGeoError("Please provide an argument")


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