import json
import os
import pprint
import sys
import time
import webbrowser
from collections import defaultdict
from os.path import splitext
from tempfile import NamedTemporaryFile

import click

from max_ard.exceptions import BadARDRequest
from max_ard.io import KmlDoc, KmzDoc, ShpDoc
from max_ard.select import Select


@click.group()
def select():
    """Tools for interacting with the ARD Select service"""
    pass


@click.command()
@click.argument("select_id", required=True)
@click.option(
    "--format",
    help="Return the status in an alternate format. `raw` will return the full response JSON.",
)
def status(select_id, format):
    """Return the status of a Select request using its SELECT_ID"""
    try:
        s = Select.from_id(select_id)
    except Exception as e:
        click.secho(str(e), fg="red", err=True)
        sys.exit()
    if format == "raw":
        click.secho(str(s.response.dict()), fg="cyan")
    else:
        fg_color = "cyan"
        err = False
        if s.state == "SUCCEEDED":
            fg_color = "green"
        elif s.state == "FAILED":
            fg_color = "red"
            err = True
        click.secho(s.state, fg=fg_color, err=err)


@click.command()
@click.argument("select_id", required=True)
@click.option(
    "--dest",
    help="Returns the select in one of the generated format types: `html`, `geojson`, `geojsonl`, `kml`, `kmz`, `shp`, `stac`.",
)
@click.option(
    "--format",
    help="If --dest is not provided, outputs to stdout in these formats:"
    + "`html`, `geojsonl`, `kml`, `stac`. Ignored if --dest is used",
)
@click.option("--verbose", "-v", is_flag=True, help="Return more information about the Select")
def describe(select_id, dest=None, format=None, verbose=False):
    """Returns info about the Select given the SELECT_ID. If no DEST is supplied, prints out a summary of the select.

    Providing a DEST writes to that destination the contents of the given Select result file generated by the system.
    Make sure to include the correct file extension in your desination filepath.
    Formats `geojson`, and `geojsonl`, `kml`, `kmz`, `shp`, and `stac` return files describing the tiles. Format `html` is an interactive map
    browser of the results - to view the map without downloading it first, use `maxard select browse SELECT_ID`"""
    try:
        s = Select.from_id(select_id)
    except Exception as e:
        click.secho(str(e), fg="red", err=True)
        sys.exit()
    pp = pprint.PrettyPrinter(indent=4)
    if not s.finished:
        click.secho("Select has not finished running", fg="cyan")
        if verbose:
            click.secho("*******************", fg="cyan")
            click.secho("Request details:", fg="cyan")
            click.secho(pp.pformat(s.request.to_payload()), fg="cyan")
            click.secho("*******************", fg="cyan")
        exit()
    if s.response.error_message is not None:
        error = s.response.error_message
        click.secho(f'Error in selection process: {error["Error"]}', fg="red", err=True)
        click.secho(f'Reason: {error["Cause"]}', fg="red", err=True)
        exit()
    if dest is None and format is None:
        click.secho("\n")
        click.secho(f"Getting Select ID {select_id}...", fg="cyan")
        click.secho("", fg="cyan")
        click.secho("*******************", fg="cyan")
        click.secho("Request details:", fg="cyan")
        click.secho(pp.pformat(s.request.to_payload()), fg="cyan")
        click.secho("*******************", fg="cyan")
        click.secho("")
        geojson = json.loads(s.get_link_contents("geojson"))
        acqs = defaultdict(list)
        count = 0
        for tile in geojson["features"]:
            for feature in tile["properties"]["best_matches"]:
                count += 1
                acqs[feature["acquisition_id"]].append(feature["date"])
        click.secho(
            f"{count} tiles identified in the following {len(acqs.keys())} acquisitions:",
            fg="green",
        )
        for acq, dates in acqs.items():
            click.secho(f"{acq} ({dates[0]}) - {len(dates)} tiles", fg="green")
        click.secho("")
        click.secho("Ordering this selection will use:", fg="cyan")
        click.secho(f"- {s.usage.area.fresh_imagery_sqkm} sqkm of fresh imagery", fg="cyan")
        click.secho(f"- {s.usage.area.standard_imagery_sqkm} sqkm of standard imagery", fg="cyan")
        click.secho(f"- {s.usage.area.training_imagery_sqkm} sqkm of training imagery", fg="cyan")
        # click.secho('', fg='cyan')
        # click.secho(f'As of {s.usage["usage_as_of"]}:', fg='cyan')
        # click.secho(f'{s.usage["available_sqkm"]} sqkm available', fg='cyan')
        click.secho("")
        exit()
    else:
        if dest:
            format = splitext(dest)[1][1:]

        if format in ["html", "geojson", "geojsonl", "stac", "kml", "kmz", "shp"]:

            # path = Path(dest)

            if (format in s.response.links.keys()) or (
                format == "kml"
            ):  # html, geojson, geojsonl, stac, kml
                if format in s.response.links.keys():
                    file = s.get_link_contents(format)
                else:  # kml
                    file = KmlDoc(s)

                if dest:
                    with open(dest, "w") as out:
                        out.write(file)
                else:
                    click.echo(file)

            if format in ["kmz", "shp"]:
                if dest is None:
                    click.secho(
                        f"Format {format} can not be sent to stdout, use --dest <filename> instead",
                        fg="red",
                        err=True,
                    )
                if format == "kmz":
                    KmzDoc(s, dest)
                else:  # shp
                    ShpDoc(s.get_link_contents("geojson"), dest)

            click.secho(f"Select result file written to {dest}", fg="green")

        else:
            click.secho(
                f'Unknown format {format}, try "html", "geojson", "geojsonl", "kml", "kmz", "shp", or "stac"',
                fg="red",
                err=True,
            )


@click.command()
@click.argument("select_id", required=True)
@click.option(
    "--format",
    help="Format of result file to get signed url for: `html`, `geojson`, geojsonl`, or `stac`",
)
def url(select_id, format):
    """Gets links for the Select result files for a Select of SELECT_ID. If no FORMAT is supplied, returns the base URL of the Select.
    If a FORMAT is supplied, returns a pre-signed URL to download the result of type FORMAT."""
    try:
        s = Select.from_id(select_id)
    except Exception as e:
        click.secho(str(e), fg="red", err=True)
        sys.exit()
    if not s.finished:
        click.secho("Select has not finished running", fg="cyan")
        sys.exit()
    if format is None:
        click.secho(s.response.links["self"], fg="green")
    elif format in s.response.links:
        click.secho(s.get_signed_link(format), fg="green")
    else:
        click.secho(
            f'Unknown format {format}, try "html", "geojson", "geojsonl", or "stac"',
            fg="red",
            err=True,
        )


@click.command()
@click.argument("select_id", required=True)
def browse(select_id):
    """Downloads the HTML interactive viewer of the Select result SELECT ID to a temporary file and opens the
    map with the system web browser.
    When exited the temporary file is deleted. Use `max-ard select describe SELECT_ID --format html > my_map.html`
    to save a local copy"""

    click.secho("Fetching map...", fg="cyan")
    try:
        s = Select.from_id(select_id)
    except Exception as e:
        click.secho(str(e), fg="red", err=True)
        sys.exit()
    if not s.finished:
        click.secho("Select has not finished running", fg="cyan")
        exit()
    prefix = f"ARD_Select_{select_id}-"
    with NamedTemporaryFile(prefix=prefix, suffix=".html", delete=False) as tmp:
        tmp.write(s.get_link_contents("html").encode(encoding="UTF-8"))
    # Windows won't let you open the file from browser while the context manager has it open
    # so we have to manually delete it
    webbrowser.open(f"file://{tmp.name}")
    time.sleep(5)
    os.unlink(tmp.name)


class NumericType(click.ParamType):
    name = "numeric"

    def convert(self, value, param, ctx):
        # strip commas if the got passed in a bbox
        value = value.replace(",", "")
        try:
            return float(value)
        except TypeError:
            self.fail(
                "expected string for int() conversion, got "
                f"{value!r} of type {type(value).__name__}",
                param,
                ctx,
            )
        except ValueError:
            self.fail(f"{value!r} is not a value that can be converted to a float", param, ctx)


NUM_TYPE = NumericType()


@click.command()
@click.option(
    "--acq-id",
    "acq_ids",
    multiple=True,
    help="Limit the Select to these acquisition IDs. Can be provided multiple times",
)
@click.option("--datetime", help="Limit the Select to a given date or date range")
@click.option(
    "--intersects",
    help="Search for tiles that intersect this geometry in WKT format, or load geometry from a file path",
)
@click.option(
    "--bbox",
    nargs=4,
    type=NUM_TYPE,
    help="like `intersects`, but limits search to a WGS84 bounding box in format `--bbox XMIN YMIN XMAX YMAX`",
)
@click.option(
    "--stack-depth",
    type=int,
    help="If provided, only return tiles where the stack depth can be fulfilled.",
)
@click.option(
    "--filter",
    nargs=3,
    multiple=True,
    help="Add a filter statement in the form of `--filter <property> <operator> <value>. See docs for full filter syntax.",
)
@click.option(
    "--image-age",
    "image_age_category",
    multiple=True,
    type=click.Choice(["fresh", "standard", "training"], case_sensitive=False),
    help="Limit imagery to an image age category. Can be used more than once if age ranges are contiguous.",
)
@click.option(
    "--min-cloud-free",
    type=NUM_TYPE,
    help="Shortcut to filter the minimum percentage of cloud-free cover. A value of 100 means an image must be 100% free of clouds.",
)
@click.option(
    "--min-data", type=NUM_TYPE, help="Shortcut to filter the minimum percentage of valid pixels."
)
@click.option(
    "--bba", is_flag=True, help="Restrict imagery to acquisitions that meet BBA requirements"
)
@click.option("--verbose", "-v", is_flag=True, help="Return more information about the Select")
def submit(**kwargs):
    """Submits a Select request to the server.

    There are numerous options and filter possibilities - consult the documentation for more information.

    At a minimum, you must supply at least one of the following:
    - a spatial filter - `intersects` or `bbox`
    - specific acquisition ID or IDs

    The Select service will try to compute results within 20 seconds. If it takes longer than 20 seconds the command
    will return a job ID that you can check for completion with `max-ard select status SELECT_ID`
    """
    # pop out kwargs that don't go the API
    min_cloud_free = kwargs.pop("min_cloud_free")
    min_data = kwargs.pop("min_data")
    filters = kwargs.pop("filter", [])
    for_bba = kwargs.pop("bba", False)
    verbose = kwargs.pop("verbose", False)

    query = defaultdict(dict)
    for prop, op, value in filters:
        # validate props and ops
        if op in ["eq", "ne", "gt", "gte", "lt", "lte"]:
            try:
                value = float(value)
            except:
                pass
        # turn comma-delimited strings to lists
        # and try to cast numbers if needed
        elif op == "in":
            value = value.split(",")
            value = [v.strip() for v in value]
            try:
                value = [float(v) for v in value]
            except:
                pass

        query[prop][op] = value

    # orders with BBA enabled will be rejected if the off-nadir angle of
    # any acquisition is greater than 30 degrees, so don't select images that
    # could cause a failure if the select is ordered
    if for_bba:
        query["view:off_nadir"]["lte"] = 30

    if min_cloud_free is not None:
        if min_cloud_free < 100:
            query["aoi:cloud_free_percentage"]["gte"] = min_cloud_free
        else:
            query["aoi:cloud_free_percentage"]["eq"] = 100

    if min_data is not None:
        if min_data < 100:
            query["aoi:data_percentage"]["gte"] = min_data
        else:
            query["aoi:data_percentage"]["eq"] = 100

    kwargs["query"] = query
    try:
        s = Select(**kwargs)
    except Exception as e:
        click.secho(f"There was an error in your parameters: {e}", fg="red", err=True)
        exit()

    try:
        s.submit()
    except BadARDRequest as e:
        click.secho(f"There was an error in your request: {e}", fg="red", err=True)
        if verbose:
            pp = pprint.PrettyPrinter(indent=4)
            click.secho("Request payload:", fg="red", err=True)
            click.secho(pp.pformat(s.request.to_payload()), fg="red", err=True)
        exit()
    if s.finished:
        click.secho(f"Select {s.select_id} has completed", fg="green")
    else:
        click.secho(f"Select {s.select_id} is still running", fg="cyan")
        click.secho(f"Run `max-ard select status {s.select_id}` to check the status", fg="cyan")
        click.secho('When the status is "SUCCEEDED" you can: ', fg="cyan")
    click.secho(
        f"Run `max-ard select describe {s.select_id}` to see a basic overview of the results",
        fg="cyan",
    )
    click.secho(
        f"Run `max-ard select browse {s.select_id}` to launch a browser-based map view of the results",
        fg="cyan",
    )
    click.secho("Run `max-ard select` to see all the `select` commands", fg="cyan")
    if verbose:
        pp = pprint.PrettyPrinter(indent=4)
        click.secho("Request payload:", fg="cyan")
        click.secho(pp.pformat(s.request.to_payload()), fg="cyan")


select.add_command(status)
select.add_command(describe)
select.add_command(browse)
select.add_command(submit)
select.add_command(url)
