"""
CWL2OGC (c) 2025

CWL2OGC is licensed under
Creative Commons Attribution-ShareAlike 4.0 International.

You should have received a copy of the license along with this work.
If not, see <https://creativecommons.org/licenses/by-sa/4.0/>.
"""

from cwl_utils.parser import (
    CommandInputParameter,
    CommandOutputParameter,
    Directory,
    EnumSchema,
    File,
    InputArraySchema,
    InputEnumSchema,
    InputParameter,
    InputRecordSchema,
    OutputArraySchema,
    OutputEnumSchema,
    OutputParameter,
    OutputRecordSchema,
    Process
)
from io import IOBase
from loguru import logger
from typing import (
    Any,
    get_args,
    get_origin,
    TypeVar,
    Union
)
import cwl_utils
import json

Stream = TypeVar('Stream', bound=IOBase)
'''A single CWL Process or a list of Processes union type.'''

Processes = TypeVar('Processes', bound=Union[Process, list[Process]])
'''A single CWL Process or a list of Processes union type.'''

__CommandInputEnumSchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandInputEnumSchema,
                                   cwl_utils.parser.cwl_v1_1.CommandInputEnumSchema,
                                   cwl_utils.parser.cwl_v1_2.CommandInputEnumSchema]

__CommandOutputEnumSchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandOutputEnumSchema,
                                    cwl_utils.parser.cwl_v1_1.CommandOutputEnumSchema,
                                    cwl_utils.parser.cwl_v1_2.CommandOutputEnumSchema]

__CommandInputRecordSchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandInputRecordSchema,
                                     cwl_utils.parser.cwl_v1_1.CommandInputRecordSchema,
                                     cwl_utils.parser.cwl_v1_2.CommandInputRecordSchema]

__CommandInputArraySchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandInputArraySchema,
                                    cwl_utils.parser.cwl_v1_1.CommandInputArraySchema,
                                    cwl_utils.parser.cwl_v1_2.CommandInputArraySchema]

__CommandOutputArraySchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandOutputArraySchema,
                                     cwl_utils.parser.cwl_v1_1.CommandOutputArraySchema,
                                     cwl_utils.parser.cwl_v1_2.CommandOutputArraySchema]

__CommandOutputRecordSchema__ = Union[cwl_utils.parser.cwl_v1_0.CommandOutputRecordSchema,
                                      cwl_utils.parser.cwl_v1_1.CommandOutputRecordSchema,
                                      cwl_utils.parser.cwl_v1_2.CommandOutputRecordSchema]

__STRING_FORMAT_URL__ = 'https://raw.githubusercontent.com/eoap/schemas/main/string_format.yaml'

__STRING_FORMATS__ = {
    'Date': "date",
    'DateTime': "date-time",
    'Duration': "duration",
    'Email': "email",
    'Hostname': "hostname",
    'IDNEmail': "idn-email",
    'IDNHostname': "idn-hostname",
    'IPv4': "ipv4",
    'IPv6': "ipv6",
    'IRI': "iri",
    'IRIReference': "iri-reference",
    'JsonPointer': "json-pointer",
    'Password': "password",
    'RelativeJsonPointer': "relative-json-pointer",
    'UUID': "uuid",
    'URI': "uri",
    'URIReference': "uri-reference",
    'URITemplate': "uri-template",
    'Time': "time"
}

class __CWLtypes2OGCConverter__:

    def _on_enum(input):
        pass

    def _on_enum_schema(input):
        pass

    def _on_array(input):
        pass

    def _on_input_array_schema(input):
        pass

    def _on_input_parameter(input):
        pass

    def _on_input(input):
        pass

    def _on_list(input):
        pass

    def _on_record(input):
        pass

    def _on_record_schema(input):
        pass

class BaseCWLtypes2OGCConverter(__CWLtypes2OGCConverter__):
    '''
    A helper class to automate the conversion of CWL input/output definitions into OGC API - Processes and JSON Schemas.
    '''
    _CWL_TYPES__ = {}

    def __init__(self, cwl: Processes):
        '''
        Initializes the converter, given the CWL document where extracting informations from.

        Args:
            `cwl` (`Processes`): The CWL document object model

        Returns:
            `None`: none.
        '''
        self.cwl = cwl

        def _map_type(type_: Any, map_function: Any) -> None:
            if isinstance(type_, list):
                for typ in type_:
                    _map_type(typ, map_function)
            elif get_origin(type_) is Union:
                for typ in get_args(type_):
                    _map_type(typ, map_function)
            else:
               self._CWL_TYPES__[type_] = map_function

        _map_type("int", lambda input : { "type": "integer", "format": "int32" })
        _map_type("long", lambda input : { "type": "integer", "format": "int64" })
        _map_type("double", lambda input : { "type": "number", "format": "double" })
        _map_type("float", lambda input : { "type": "number", "format": "float" })
        _map_type("boolean", lambda input : { "type": "boolean" })
        _map_type(["string", "stdout"], lambda input : { "type": "string" })

        _map_type(["File", File, "Directory", Directory], lambda input : { "type": "string", "format": "uri" })

        # these are not correctly interpreted as CWL types
        _map_type("record", self._on_record)
        _map_type("enum", self._on_enum)
        _map_type("array", self._on_array)

        _map_type(list, self._on_list)

        _map_type([__CommandInputEnumSchema__,
                   __CommandOutputEnumSchema__,
                   EnumSchema,
                   InputEnumSchema,
                   OutputEnumSchema], self._on_enum_schema)

        _map_type([CommandInputParameter,
                   CommandOutputParameter,
                   InputParameter,
                   OutputParameter], self._on_input_parameter)

        _map_type([__CommandInputArraySchema__,
                   __CommandOutputArraySchema__,
                   InputArraySchema,
                   OutputArraySchema], self._on_input_array_schema)

        _map_type([__CommandInputRecordSchema__,
                   __CommandOutputRecordSchema__,
                   InputRecordSchema,
                   OutputRecordSchema], self._on_record_schema)

    def _clean_name(self, name: str) -> str:
        return name[name.rfind('/') + 1:]

    def _is_nullable(self, input):
        return hasattr(input, "type_") and  isinstance(input.type_, list) and "null" in input.type_

    # enum

    def _on_enum_internal(self, symbols):
        return {
            "type": "string",
            "enum": list(map(lambda symbol : self._clean_name(symbol), symbols))
        }

    def _on_enum_schema(self, input):
        return self._on_enum_internal(input.type_.symbols)

    def _on_enum(self, input):
        return self._on_enum_internal(input.symbols)

    def _on_array_internal(self, items):
        return {
            "type": "array",
            "items": self._on_input(items)
        }

    def _on_array(self, input):
        return self._on_array_internal(input.items)

    def _on_input_array_schema(self, input):
        return self._on_array_internal(input.type_.items)

    def _on_input_parameter(self, input):
        logger.warning(f"input_parameter not supported yet: {input}")
        return {}

    def _warn_unsupported_type(self, typ: Any):
        supported_types = '\n * '.join([str(k) for k in list(self._CWL_TYPES__.keys())])
        logger.warning(f"{typ} not supported yet, currently supporting only:\n * {supported_types}")

    def _search_type_in_dictionary(self, expected):
        for requirement in getattr(self.cwl, "requirements", []):
            if ("SchemaDefRequirement" == requirement.class_):
                for type in requirement.types:
                    if (expected == type.name):
                        return self._on_input(type)

        self._warn_unsupported_type(expected)
        return {}

    def _on_input(self, input):
        type = {}

        if isinstance(input, str):
            if input in self._CWL_TYPES__:
                type = self._CWL_TYPES__.get(input)(input)
            else:
                type = self._search_type_in_dictionary(input)
        elif hasattr(input, "type_"):
            if isinstance(input.type_, str):
                if input.type_ in self._CWL_TYPES__:
                    type = self._CWL_TYPES__.get(input.type_)(input)
                else:
                    type = self._search_type_in_dictionary(input.type_)
            elif input.type_.__class__ in self._CWL_TYPES__:
                type = self._CWL_TYPES__.get(input.type_.__class__)(input)
            else:
                self._warn_unsupported_type(input.type_)
        else:
            logger.warning(f"I still don't know what to do for {input}")

        if hasattr(input, "default") and input.default:
            type["default"] = input.default

        return type

    def _on_list(self, input):
        input_list = {
            "nullable": self._is_nullable(input)
        }

        inputs_schema = list(
            map(
                lambda item: self._on_input(item),
                filter(
                    lambda current: "null" != current,
                    input.type_
                )
            )
        )

        if 1 == len(inputs_schema):
            input_list.update(inputs_schema[0])
        else:
            input_list["anyOf"] = inputs_schema

        return input_list

    # record

    def _on_record_internal(self, record, fields):
        record_name = ''
        if hasattr(record, "name"):
            record_name = record.name
        elif hasattr(record, "id"):
            record_name = record.id
        else:
            logger.warning(f"Impossible to detect {record.__dict__}, skipping name check...")

        if __STRING_FORMAT_URL__ in record_name:
            return { "type": "string", "format": __STRING_FORMATS__.get(record.name.split('#')[-1]) }

        record = {
            "type": "object",
            "properties": {},
            "required": []
        }

        for field in fields:
            field_id = self._clean_name(field.name)
            record["properties"][field_id] = self._on_input(field)

            if not self._is_nullable(field):
                record["required"].append(field_id)

        return record

    def _on_record_schema(self, input):
        return self._on_record_internal(input, input.type_.fields)

    def _on_record(self, input):
        return self._on_record_internal(input, input.fields)

    def _type_to_string(self, typ: Any) -> str:
        if get_origin(typ) is Union:
            return " or ".join([self._type_to_string(inner_type) for inner_type in get_args(typ)])

        if isinstance(typ, list):
            return f"[ {', '.join([self._type_to_string(t) for t in typ])} ]"

        if hasattr(typ, "items"):
            return f"{self._type_to_string(typ.items)}[]"

        if hasattr(typ, "symbols"):
             return f"enum[ {', '.join([s.split('/')[-1] for s in typ.symbols])} ]"

        if hasattr(typ, 'type_'):
            return self._type_to_string(typ.type_)

        if isinstance(typ, str):
            return typ
        
        return typ.__name__

    def _to_ogc(self, params, is_input: bool = False) -> dict:
        ogc_map = {}

        for param in params:
            schema = {
                "schema": self._on_input(param),
                "metadata": [ { "title": "cwl:type", "value": f"{self._type_to_string(param.type_)}" } ]
            }

            if is_input:
                schema["minOccurs"] = 0 if self._is_nullable(param) else 1
                schema["maxOccurs"] = 1
                schema["valuePassing"] = "byValue"

            if param.label:
                schema["title"] = param.label

            if param.doc:
                schema["description"] = param.doc

            ogc_map[self._clean_name(param.id)] = schema

        return ogc_map

    def get_inputs(self) -> dict:
        '''
        Returns a dictionary representing OGC API - Processes inputs in-memory structure.

        Returns:
            `dict`: The generated dictionary representing OGC API - Processes inputs in-memory structure.
        '''
        return self._to_ogc(params=self.cwl.inputs, is_input=True)

    def get_outputs(self) -> dict:
        '''
        Returns a dictionary representing OGC API - Processes outputs in-memory structure.

        Returns:
            `dict`: The generated dictionary representing OGC API - Processes inputs in-memory structure.
        '''
        return self._to_ogc(params=self.cwl.outputs)

    def _to_json_schema(self, parameters: dict, label: str) -> dict:
        id = self.cwl.id.split('#')[-1]

        schema = {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "$id": f"https://eoap.github.io/cwl2ogc/{id}/{label}.yaml",
            "description": f"The schema to represent a {id} {label} definition",
            "type": "object",
            "required": [],
            "properties": {},
            "additionalProperties": False,
            "$defs": {}
        }

        for k, v in parameters.items():
            schema["properties"][k] = { "$ref": f"#/$defs/{k}" }

            property_schema = v["schema"]
            schema["$defs"][k] = property_schema

            if "nullable" not in property_schema or not property_schema["nullable"]:
                schema["required"].append(k)

        return schema

    def get_inputs_json_schema(self) -> dict:
        '''
        Returns a dictionary representing the inputs JSON Schema in-memory structure.

        Returns:
            `dict`: The generated dictionary the inputs JSON Schema in-memory structure.
        '''
        return self._to_json_schema(self.get_inputs(), "inputs")
    
    def get_outputs_json_schema(self) -> dict:
        '''
        Returns a dictionary representing the outputs JSON Schema in-memory structure.

        Returns:
            `dict`: The generated dictionary representing the outputs JSON Schema in-memory structure.
        '''
        return self._to_json_schema(self.get_outputs(), "outputs")

    def _dump(self, data: dict, stream: Stream, pretty_print: bool):
        json.dump(data, stream, indent=2 if pretty_print else None)

    def dump_inputs(self, stream: Stream, pretty_print: bool = False):
        '''
        Dumps the OGC API - Processes inputs schema to its JSON representation.

        Args:
            `stream` (`Stream`): The stream where serializing the JSON representation
            `pretty_print` (`bool`): formats the output if `True`, in a single line otherwise. Default is `False`

        Returns:
            `None`: none.
        '''
        self._dump(data=self.get_inputs(), stream=stream, pretty_print=pretty_print)

    def dump_outputs(self, stream: Stream, pretty_print: bool = False):
        '''
        Dumps the OGC API - Processes outputs schema to its JSON representation.

        Args:
            `stream` (`Stream`): The stream where serializing the JSON representation
            `pretty_print` (`bool`): formats the output if `True`, in a single line otherwise. Default is `False`

        Returns:
            `None`: none.
        '''
        self._dump(data=self.get_outputs(), stream=stream, pretty_print=pretty_print)

    def dump_inputs_json_schema(self, stream: Stream, pretty_print: bool = False):
        '''
        Dumps the inputs JSON Schema to its JSON representation.

        Args:
            `stream` (`Stream`): The stream where serializing the JSON representation
            `pretty_print` (`bool`): formats the output if `True`, in a single line otherwise. Default is `False`

        Returns:
            `None`: none.
        '''
        self._dump(data=self.get_inputs_json_schema(), stream=stream, pretty_print=pretty_print)

    def dump_outputs_json_schema(self, stream: Stream, pretty_print: bool = False):
        '''
        Dumps the outputs JSON Schema to its JSON representation.

        Args:
            `stream` (`Stream`): The stream where serializing the JSON representation
            `pretty_print` (`bool`): formats the output if `True`, in a single line otherwise. Default is `False`

        Returns:
            `None`: none.
        '''
        self._dump(data=self.get_outputs_json_schema(), stream=stream, pretty_print=pretty_print)
