"""The Bundle object is the root object for a bundle, which includes accessors
for partitions, schema, and the filesystem.

Copyright (c) 2015 Civic Knowledge. This file is licensed under the terms of
the Revised BSD License, included in this distribution as LICENSE.txt

"""

import os
import sys
from time import time
import traceback
from decorator import decorator
from six import string_types, iteritems, u, b
from fs.errors import NoSysPathError
from fs.wrapfs.lazyfs import LazyFS

from geoid.civick import GVid
from geoid import NotASummaryName
from ambry.dbexceptions import BuildError, BundleError, FatalError
from ambry.orm import File
import ambry.etl
from ..util import get_logger, Constant
from .process import ProcessLogger

indent = '    '  # Indent for structured log output

BUILD_LOG_FILE = 'log/build_log{}.txt'


def _CaptureException(f, *args, **kwargs):
    """Decorator implementation for capturing exceptions."""
    from ambry.dbexceptions import LoggedException

    b = args[0]  # The 'self' argument

    try:
        return f(*args, **kwargs)
    except Exception as e:
        raise
        try:
            b.set_error_state()
            b.commit()
        except Exception as e2:
            b.log('Failed to set bundle error state: {}'.format(e))
            raise e

        if b.capture_exceptions:
            b.logged_exception(e)
            raise LoggedException(e, b)
        else:
            b.exception(e)
            raise


def CaptureException(f, *args, **kwargs):
    """Decorator to capture exceptions. and logging the error"""
    return decorator(_CaptureException, f)  # Preserves signature


class Bundle(object):
    STATES = Constant()
    STATES.NEW = 'new'
    STATES.SYNCED = 'synced'
    STATES.DOWNLOADED = 'downloaded'
    STATES.CLEANING = 'clean'
    STATES.CLEANED = 'cleaned'
    STATES.PREPARING = 'prepare'
    STATES.PREPARED = 'prepared'
    STATES.WAITING = 'waiting'
    STATES.BUILDING = 'building'
    STATES.BUILT = 'built'
    STATES.FINALIZING = 'finalizing'
    STATES.FINALIZED = 'finalized'
    STATES.INSTALLING = 'install'
    STATES.INSTALLED = 'installed'
    STATES.CHECKEDIN = 'checkedin'
    STATES.CHECKEDOUT = 'checkedout'
    STATES.META = 'meta'
    STATES.SCHEMA = 'schema'
    STATES.INGESTING = 'ingest'
    STATES.INGESTED = 'ingested'
    STATES.NOTINGESTABLE = 'not_ingestable'
    STATES.SOURCE = 'source'

    # Other things that can be part of the 'last action'
    STATES.INFO = 'info'

    # If the bundle is in test mode, only run 1000 rows, selected from the first 100,000
    TEST_ROWS = 1000  # Number of test rows to select from file.

    #  Default body content for pipelines
    default_pipelines = {

        'build': {
            'first': [],
            'source_map': [ambry.etl.MapSourceHeaders],
            'cast': [ambry.etl.CastColumns],
            'body': [],
            'last': [],
            'select_partition': [ambry.etl.SelectPartition],
            'write': [ambry.etl.WriteToPartition],
            'final': []
        },
    }

    def __init__(self, dataset, library, source_url=None, build_url=None):
        import logging

        self._dataset = dataset
        self._vid = dataset.vid
        self._library = library
        self._logger = None

        self._log_level = logging.INFO

        self._errors = []
        self._warnings = []

        self._source_url = source_url
        self._build_url = build_url

        self._pipeline_editor = None  # A function that can be set to edit the pipeline, rather than overriding the method

        self._source_fs = None
        self._build_fs = None

        self._identity = None

        self.stage = None  # Set to the current ingest or build stage

        self.limited_run = False
        self.capture_exceptions = False  # If set to true (in CLI), will catch and log exceptions internally.
        self.exit_on_fatal = True
        self.multi = None  # Number of multiprocessing processes
        self.is_subprocess = False  # Externally set in child processes.
        # AMBRY_IS_REMOTE is set in the docker file for the builder container
        self.is_remote_process = os.getenv('AMBRY_IS_REMOTE', False)

        # Test class imported from the test.py file
        self.test_class = None

        self._progress = None
        self._ps = None  # Progress logger section, created as needed.
        self.init()

    def init(self):
        """An overridable initialization method, called in the Bundle constructor"""
        p
ass

    def set_file_system(self, source_url=False, build_url=False):
        """Set the source file filesystem and/or build  file system"""

        assert isinstance(source_url, string_types) or source_url is None or source_url is False
        assert isinstance(build_url, string_types) or build_url is False

        if source_url:
            self._source_url = source_url
            self.dataset.config.library.source.url = self._source_url
            self._source_fs = Non
e

        elif source_url is None:
            self._source_url = None
            self.dataset.config.library.source.url = self._source_url
            self._source_fs = None

        if build_url:
            self._build_url = build_url
            self.dataset.config.library.build.url = self._build_url
            self._build_fs = None

        self.dataset.commit()

    def clear_file_systems(self):
        """Remove references to build and source file systems, reverting to the defaults"""

        self._source_url = None
        self.dataset.config.library.source.url = None
        self._source_fs = None

        self._build_url = None
        self.dataset.config.library.build.url = None
        self._build_fs = None

        self.dataset.commit()

    def cast_to_subclass(self):
        """
        Load the bundle file from the database to get the derived bundle class,
        then return a new bundle built on that class

        :return:
        """
        self.import_lib()
        self.load_requirements()

        try:
            self.commit()  # To ensure the rollback() doesn't clear out anything important
            bsf = self.build_source_files.file(File.BSFILE.BUILD)
        except Exception as e:
            self.log('Error trying to create a bundle source file ... {} '.format(e))
            raise
            self.rollback()
            return self

        try:
            clz = bsf.import_bundle()

        except Exception as e:

            raise BundleError('Failed to load bundle code file, skipping : {}'.format(e))

        b = clz(self._dataset, self._library, self._source_url, self._build_url)
        b.limited_run = self.limited_run
        b.capture_exceptions = self.capture_exceptions
        b.multi = self.multi


        return b

    def import_lib(self):
        """Import the lib.py file from the bundle"""
        return self.build_source_files.file(File.BSFILE.LIB).import_lib()

    def load_requirements(self):
        """If there are python library requirements set, append the python dir
        to the path."""

        for module_name, pip_name in iteritems(self.metadata.requirements):
            extant = self.dataset.config.requirements[module_name].url

            force = (extant and extant != pip_name)

            self._library.install_packages(module_name, pip_name, force=force)

            self.dataset.config.requirements[module_name].url = pip_name

        python_dir = self._library.filesystem.python()
        sys.path.append(python_dir)

    def commit(self):
        return self.dataset.commit()

    def close(self):
        self.progress.close()
        self.dataset.close()



    def close_session(self):
        return self.dataset.close_session()

    @property
    def session(self):
        return self.dataset.session

    def rollback(self):
        return self.dataset.rollback()

    @property
    def dataset(self):
        from sqlalchemy import inspect

        if inspect(self._dataset).detached:
            vid = self._vid
            self._dataset = self._dataset._database.dataset(vid)
            assert self._dataset._database # New
        else:
            a