#!python3
# -*- coding: utf-8 -*-
"""
Code to upload metadata to the CKAN server to create/update an existing package
The metadata is defined by the user in an Excel worksheet
This file implements functions to initiate a DataStore without uploading any data.
"""
from typing import List, Union, Generator
import io

import pandas as pd

from ckanapi_harvesters.auxiliary.error_level_message import ContextErrorLevelMessage, ErrorLevel
from ckanapi_harvesters.builder.builder_resource import builder_request_default_auth_if_ckan, initial_resource_building_state
from ckanapi_harvesters.builder.builder_resource_datastore_file import BuilderDataStoreFile
from ckanapi_harvesters.builder.builder_resource_multi_abc import FileChunkDataFrame
from ckanapi_harvesters.auxiliary.ckan_errors import NotMappedObjectNameError, DataStoreNotFoundError
from ckanapi_harvesters.auxiliary.list_records import ListRecords
from ckanapi_harvesters.builder.builder_errors import ResourceFileNotExistMessage
from ckanapi_harvesters.auxiliary.ckan_model import CkanResourceInfo
from ckanapi_harvesters.auxiliary.ckan_errors import CkanArgumentError, FunctionMissingArgumentError
from ckanapi_harvesters.ckan_api import CkanApi
from ckanapi_harvesters.auxiliary.ckan_auxiliary import _string_from_element, assert_or_raise, datastore_id_col


class BuilderDataStoreUrl(BuilderDataStoreFile):  #, BuilderUrlABC):  # multiple inheritance can give undefined results
    """
    Class representing a DataStore (resource metadata and fields metadata) defined by a url.
    """
    def __init__(self, *, parent, name:str=None, format:str=None, description:str=None,
                 resource_id:str=None, download_url:str=None, url:str=None, options_string:str=None, base_dir:str=None):
        super(BuilderDataStoreFile, self).__init__(parent=parent, name=name, format=format, description=description, resource_id=resource_id,
                                                   download_url=download_url, options_string=options_string, base_dir=base_dir)
        # super(BuilderUrlABC, self).__init__(name=name, format=format, description=description, resource_id=resource_id, download_url=download_url, url=url)
        self.reupload_on_update = False
        self.reupload_if_needed = False
        self.url:str = url
        self.file_name = name

    def copy(self, *, dest=None, parent=None):
        if dest is None:
            dest = BuilderDataStoreUrl(parent=self.parent_package)
        super().copy(dest=dest, parent=parent)
        dest.reupload_on_update = self.reupload_on_update
        dest.reupload_if_needed = self.reupload_if_needed
        dest.url = self.url
        dest.file_name = self.file_name
        return dest

    def _load_from_df_row(self, row: pd.Series, base_dir:str=None):
        super(BuilderDataStoreFile, self)._load_from_df_row(row=row)
        # super(BuilderUrlABC, self)._load_from_df_row(row=row)
        self.url: str = _string_from_element(row["file/url"], strip=True)
        self.file_name = self.name

    @staticmethod
    def sample_file_path_is_url() -> bool:
        return True

    def get_sample_file_path(self, resources_base_dir: str, ckan:Union[CkanApi,None]=None, file_index:int=0) -> str:
        return self.url

    def init_local_files_list(self, resources_base_dir:str, cancel_if_present:bool=True, **kwargs) -> List[str]:
        file_path = self.get_sample_file_path(resources_base_dir=resources_base_dir)
        self.file_size = 1
        self.local_file_list = [file_path]
        self.local_file_size = [self.file_size]
        self.local_file_size_sum = 1
        return self.local_file_list

    def load_sample_data(self, resources_base_dir:str, *, ckan:CkanApi=None,
                         proxies:dict=None, headers:dict=None) -> bytes:
        self.sample_source = self.url
        if ckan is None:
            raise FunctionMissingArgumentError("BuilderDataStoreUrl.load_sample_data", "ckan")
        return ckan.download_url_proxy(self.url, proxies=proxies, headers=headers, auth_if_ckan=builder_request_default_auth_if_ckan).content

    def get_local_df_chunk_generator(self, resources_base_dir:str, ckan:CkanApi, **kwargs) -> Generator[FileChunkDataFrame, None, None]:
        payload = self.load_sample_data(resources_base_dir=resources_base_dir)
        with io.StringIO(payload.decode()) as stream:
            self.read_line_counter = 0
            with self.local_file_format.read_buffer_full(stream, fields=self._get_fields_info()) as df_file:
                if isinstance(df_file, pd.DataFrame) or isinstance(df_file, ListRecords):
                    self.file_semaphore.acquire()
                    self.read_line_counter += len(df_file)
                    line_counter = self.read_line_counter
                    self.file_semaphore.release()
                    yield FileChunkDataFrame(df_file, self.url, 0, 0, 0, line_counter)
                else:  # iterator
                    with df_file as df_generator:
                        if hasattr(df_file, "handles"):
                            file_handle = df_file.handles.handle
                        else:
                            file_handle = None
                        previous_file_position = 0
                        # for chunk_index, df in enumerate(df_generator):
                        chunk_index = 0
                        file_position = 0
                        while True:
                            self.file_semaphore.acquire()
                            if file_handle is not None:
                                file_position = file_handle.buffer.tell()  # approximative position in file
                            else:
                                file_position = 0  # no position tracking available
                            try:
                                df = next(df_generator)
                                if file_handle is None and hasattr(df, "attrs") and "file_position" in df.attrs:
                                    file_position = df.attrs["file_position"]
                                self.read_line_counter += len(df)
                                line_counter = self.read_line_counter
                            except StopIteration:
                                self.file_semaphore.release()
                                break
                            self.file_semaphore.release()
                            yield FileChunkDataFrame(df, self.url, 0, chunk_index, previous_file_position, line_counter)
                            previous_file_position = file_position
                            chunk_index = chunk_index + 1

    def upload_request_full(self, ckan:CkanApi, resources_base_dir:str, *,
                            threads:int=1, external_stop_event=None,
                            start_index:int=0, end_index:int=None, **kwargs) -> None:
        # CKAN manages URL resources: no upload is necessary
        return

    @staticmethod
    def resource_mode_str() -> str:
        return "DataStore from URL"

    def _to_dict(self, include_id:bool=True) -> dict:
        d = super()._to_dict(include_id=include_id)
        d["File/URL"] = self.url
        return d

    def upload_file_checks(self, *, resources_base_dir:str=None, ckan: CkanApi=None, **kwargs) -> Union[None,ContextErrorLevelMessage]:
        if ckan is None:
            return ResourceFileNotExistMessage(self.name, ErrorLevel.Warning, "Could not determine if resource url exists because ckan argument was not provided.")
        else:
            return ckan.download_url_proxy_test_head(self.url, **kwargs)

    def patch_request(self, ckan: CkanApi, package_id: str, *,
                      df_upload:pd.DataFrame=None, payload:Union[bytes, io.BufferedIOBase]=None,
                      reupload: bool = None, override_ckan:bool=False, resources_base_dir:str=None) -> CkanResourceInfo:
        """
        Specific implementation of patch_request which does not upload any data and only updates the fields currently present in the database

        :param resources_base_dir:
        :param ckan:
        :param package_id:
        :param reupload:
        :return:
        """
        self._merge_resource_attributes(override_ckan=override_ckan)
        if reupload is None: reupload = self.reupload_on_update
        if payload is not None or df_upload is not None:
            raise CkanArgumentError("payload", "datastore defined from URL patch")
        resource_id = self.get_or_query_resource_id(ckan, error_not_found=False)
        try:
            df_download = self.download_resource_df(ckan, download_alter=False, search_all=False, limit=1)
            if df_download is None:
                assert_or_raise(resource_id is None, RuntimeError("Unexpected: resource_id should be None"))
                raise NotMappedObjectNameError(self.name)
            current_df_fields = set(df_download.columns)
        except NotMappedObjectNameError as e:
            df_download = None
            current_df_fields = set()
        except DataStoreNotFoundError as e:
            df_download = None
            current_df_fields = set()
        empty_datastore = df_download is None or len(df_download) == 0
        data_cleaner_fields = None
        data_cleaner_index = set()
        current_df_fields -= {datastore_id_col}  # _id does not require documentation
        aliases = self._get_alias_list(ckan)
        self._check_necessary_fields(current_df_fields, raise_error=False, empty_datastore=empty_datastore)
        self._check_undocumented_fields(current_df_fields)
        primary_key, indexes = self._get_primary_key_indexes(data_cleaner_index, current_df_fields=current_df_fields,
                                                             error_missing=False, empty_datastore=empty_datastore)
        fields_update = self._get_fields_update(ckan, current_df_fields=current_df_fields, data_cleaner_fields=data_cleaner_fields,
                                                reupload=reupload, override_ckan=override_ckan)
        fields = list(fields_update.values()) if len(fields_update) > 0 else None
        resource_info = ckan.resource_create(package_id, name=self.name, format=self.resource_attributes.format, description=self.resource_attributes.description,
                                             state=initial_resource_building_state if initial_resource_building_state is not None else self.resource_attributes.state,
                                             url=self.url,
                                             datastore_create=False, auto_submit=False, create_default_view=self.create_default_view,
                                             cancel_if_exists=True, update_if_exists=True, aliases=aliases, reupload=False, data_cleaner=self.data_cleaner_upload,
                                             progress_callback=self.progress_callback)
        resource_id = resource_info.id
        self.known_id = resource_id
        self._compare_fields_to_datastore_info(resource_info, current_df_fields, ckan)
        if reupload:
            # re-initialize datastore to reupload from url
            # normally, data was automatically submitted to DataStore on resource_create (not needed)
            ckan.datastore_create(resource_id, fields=fields, primary_key=primary_key, indexes=indexes, aliases=aliases)
            ckan.datastore_submit(resource_id)
        return resource_info



