# This file is part of datacube-ows, part of the Open Data Cube project.
# See https://opendatacube.org for more information.
#
# Copyright (c) 2017-2024 OWS Contributors
# SPDX-License-Identifier: Apache-2.0

import logging
from typing import Any

from flask import request
from ows.common import WGS84BoundingBox
from ows.common.v20.decoders import kvp_decode_get_capabilities
from ows.gml import Grid, IrregularAxis, RegularAxis, SpatioTemporalType
from ows.swe import Field
from ows.wcs import CoverageDescription, CoverageSummary, ServiceCapabilities
from ows.wcs.v20 import encoders as encoders_v20
from ows.wcs.v20.decoders import kvp_decode_describe_coverage, kvp_decode_get_coverage
from ows.wcs.v21 import encoders as encoders_v21

from datacube_ows.http_utils import (
    cache_control_headers,
    get_service_base_url,
    json_response,
    resp_headers,
)
from datacube_ows.ogc_exceptions import WCS2Exception
from datacube_ows.ows_configuration import OWSConfig, get_config
from datacube_ows.query_profiler import QueryProfiler
from datacube_ows.utils import log_call
from datacube_ows.wcs2_utils import get_coverage_data

_LOG: logging.Logger = logging.getLogger(__name__)

WCS_REQUESTS = ("DESCRIBECOVERAGE", "GETCOVERAGE")


@log_call
def handle_wcs2(nocase_args) -> tuple:
    operation = nocase_args.get("request", "").upper()
    if not operation:
        raise WCS2Exception("No operation specified", locator="Request parameter")
    if operation == "GETCAPABILITIES":
        return get_capabilities(nocase_args)
    if operation == "DESCRIBECOVERAGE":
        return desc_coverages(nocase_args)
    if operation == "GETCOVERAGE":
        return get_coverage(
            request.args.lists(),
            bool(nocase_args.get("ows_stats")),
            nocase_args.get("styles"),
        )
    raise WCS2Exception(
        f"Unrecognised operation: {operation}", locator="Request parameter"
    )


@log_call
def get_capabilities(args: dict) -> tuple[Any, int, dict]:
    # Extract layer metadata from Datacube.
    cfg = get_config()
    url = args.get("Host", args["url_root"])
    base_url = get_service_base_url(cfg.allowed_urls, url)

    request_obj = kvp_decode_get_capabilities(args)
    sections = request_obj.sections or ["all"]

    # TODO: check for invalid sections
    include_service_identification = False
    include_service_provider = False
    include_operations_metadata = False
    include_service_metadata = False
    include_coverage_summary = False

    if "all" in sections:
        include_service_identification = True
        include_service_provider = True
        include_operations_metadata = True
        include_service_metadata = True
        include_coverage_summary = True
    if "serviceidentification" in sections:
        include_service_identification = True
    if "serviceprovider" in sections:
        include_service_provider = True
    if "operationsmetadata" in sections:
        include_operations_metadata = True
    if "servicemetadata" in sections:
        include_service_metadata = True
    if "coveragesummary" in sections:
        include_coverage_summary = True

    assert cfg.contact_info is not None
    assert cfg.contact_info.address is not None
    capabilities = ServiceCapabilities.with_defaults_v20(
        service_url=base_url + "/wcs",
        allowed_operations=["GetCapabilities", "DescribeCoverage", "GetCoverage"],
        allow_post=False,
        title=cfg.title,
        abstract=cfg.abstract,
        keywords=cfg.keywords,
        fees=cfg.fees,
        access_constraints=[cfg.access_constraints],
        provider_name="",
        provider_site="",
        individual_name=cfg.contact_info.person,
        organisation_name=cfg.contact_info.organisation,
        position_name=cfg.contact_info.position,
        phone_voice=cfg.contact_info.telephone,
        phone_facsimile=cfg.contact_info.fax,
        delivery_point=cfg.contact_info.address.address,
        city=cfg.contact_info.address.city,
        administrative_area=cfg.contact_info.address.state,
        postal_code=cfg.contact_info.address.postcode,
        country=cfg.contact_info.address.country,
        electronic_mail_address=cfg.contact_info.email,
        online_resource=base_url,
        # hours_of_service=,
        # contact_instructions=,
        # role=,
        coverage_summaries=[
            CoverageSummary(
                identifier=product.name,
                coverage_subtype="RectifiedGridCoverage",
                title=product.title,
                wgs84_bbox=WGS84BoundingBox(
                    [
                        product.ranges.lon.min,
                        product.ranges.lat.min,
                        product.ranges.lon.max,
                        product.ranges.lat.max,
                    ]
                ),
            )
            for product in cfg.layer_index.values()
            if product.ready and not product.hide and product.wcs
        ],
        formats_supported=[fmt.mime for fmt in cfg.wcs_formats if 2 in fmt.renderers],
        crss_supported=list(cfg.published_CRSs),  # TODO: conversion to URL format
        interpolations_supported=None,  # TODO: find out interpolations
    )
    result = encoders_v20.xml_encode_capabilities(
        capabilities,
        include_service_identification=include_service_identification,
        include_service_provider=include_service_provider,
        include_operations_metadata=include_operations_metadata,
        include_service_metadata=include_service_metadata,
        include_coverage_summary=include_coverage_summary,
    )

    headers = cache_control_headers(cfg.wms_cap_cache_age)
    headers["Content-Type"] = result.content_type
    return (result.value, 200, cfg.response_headers(headers))


def create_coverage_description(cfg: OWSConfig, product) -> CoverageDescription:
    axes = [
        RegularAxis(
            label=product.native_CRS_def["horizontal_coord"],
            index_label="i",
            lower_bound=min(
                product.ranges.bboxes[product.native_CRS]["left"],
                product.ranges.bboxes[product.native_CRS]["right"],
            ),
            upper_bound=max(
                product.ranges.bboxes[product.native_CRS]["left"],
                product.ranges.bboxes[product.native_CRS]["right"],
            ),
            resolution=product.resolution_x,
            uom="deg",
            size=product.grid_high_x,
        ),
        RegularAxis(
            label=product.native_CRS_def["vertical_coord"],
            index_label="j",
            lower_bound=min(
                product.ranges.bboxes[product.native_CRS]["top"],
                product.ranges.bboxes[product.native_CRS]["bottom"],
            ),
            upper_bound=max(
                product.ranges.bboxes[product.native_CRS]["top"],
                product.ranges.bboxes[product.native_CRS]["bottom"],
            ),
            resolution=product.resolution_y,
            uom="deg",
            size=product.grid_high_y,
        ),
    ]

    # swap axes if necessary
    if product.native_CRS_def.get("vertical_coord_first"):
        axes.reverse()

    if product.regular_time_axis:
        start, end = product.time_range()
        size = (end - start).days // product.time_axis_interval
        axes.append(
            RegularAxis(
                label="time",
                index_label="k",
                lower_bound=start.isoformat(),
                upper_bound=end.isoformat(),
                resolution=f"P{product.time_axis_interval}D",
                uom="ISO-8601",
                type=SpatioTemporalType.TEMPORAL,
                size=size,
            )
        )
    elif product.time_resolution.is_subday():
        axes.append(
            IrregularAxis(
                label="time",
                index_label="k",
                positions=[f"{t.isoformat()}" for t in product.ranges.times],
                uom="ISO-8601",
                type=SpatioTemporalType.TEMPORAL,
            )
        )
    else:
        axes.append(
            IrregularAxis(
                label="time",
                index_label="k",
                positions=[
                    f"{t.isoformat()}T00:00:00.000Z" for t in product.ranges.times
                ],
                uom="ISO-8601",
                type=SpatioTemporalType.TEMPORAL,
            )
        )

    return CoverageDescription(
        identifier=product.name,
        title=product.title,
        abstract=product.definition.get("description"),
        range_type=[
            Field(
                name=band_label,
                description=band_label,
                uom=band_label,
                nil_values=dict.fromkeys(
                    product.band_idx.band_nodata_vals(), "invalid"
                ),
            )
            for band_label in product.band_idx.band_labels()
        ],
        grid=Grid(axes=axes, srs=product.native_CRS),
        native_format=cfg.wcs_formats_by_name[product.native_format].mime,
        coverage_subtype="RectifiedGridCoverage",
    )


@log_call
def desc_coverages(args) -> tuple:
    cfg = get_config()

    request_obj = kvp_decode_describe_coverage(args)

    products = []

    for coverage_id in request_obj.coverage_ids:
        product = cfg.layer_index.get(coverage_id)
        if product and product.wcs:
            products.append(product)
        else:
            raise WCS2Exception(
                f"Invalid coverage: {coverage_id}",
                WCS2Exception.NO_SUCH_COVERAGE,
                locator=coverage_id,
            )

    coverage_descriptions = [
        create_coverage_description(cfg, product) for product in products
    ]

    version = request_obj.version

    if version == (2, 0):
        result = encoders_v20.xml_encode_coverage_descriptions(coverage_descriptions)
    elif version == (2, 1):
        result = encoders_v21.xml_encode_coverage_descriptions(coverage_descriptions)
    else:
        raise WCS2Exception(
            f"Unsupported version: {version}",
            WCS2Exception.INVALID_PARAMETER_VALUE,
            locator="version",
        )
    min_cache_age = min(p.resource_limits.wcs_desc_cache_rule for p in products)
    headers = cache_control_headers(min_cache_age)
    headers["Content-Type"] = result.content_type
    return (result.value, 200, resp_headers(headers))


@log_call
def get_coverage(
    args, ows_stats: bool = False, styles=None
) -> tuple[str | bytes, int, dict[str, str]]:
    request_obj = kvp_decode_get_coverage(args)
    qprof = QueryProfiler(ows_stats)
    output, headers = get_coverage_data(request_obj, styles, qprof)
    if ows_stats:
        return json_response(qprof.profile())
    return (output, 200, resp_headers(headers))
