# -*- coding: utf-8 -*-

""" Synchronisation - Controllers

    @author: Amer Tahir

"""

module = "admin"
module_name = T("Synchronization")

log_table = "sync_log"
conflict_table = "sync_conflict"
sync_peer = None
sync_policy = None
import_export_format = "xml"

# Options Menu (available in all Functions' Views)
# - can Insert/Delete items from default menus within a function, if required.
response.menu_options = admin_menu_options

# Web2Py Tools functions
def call():
    "Call an XMLRPC, JSONRPC or RSS service"
    # Sync webservices don't use sessions, so avoid cluttering up the storage
    session.forget()
    return service()


# -----------------------------------------------------------------------------
# S3 framework functions
def index():
    "Module's Home Page"

    return dict(module_name=module_name)

import urllib2


# -----------------------------------------------------------------------------
class RequestWithMethod(urllib2.Request):

    """ ???

        @todo: add doc string

    """

    def __init__(self, method, *args, **kwargs):

        """ docstring? """

        self._method = method
        urllib2.Request.__init__(self, *args, **kwargs)


    def get_method(self):

        """ docstring? """

        return self._method


# -----------------------------------------------------------------------------
class Error:

    """ Indicates an HTTP error """

    def __init__(self, url, errcode, errmsg, headers, body=None):

        """ docstring??? """

        self.url = url
        self.errcode = errcode
        self.errmsg = errmsg
        self.headers = headers
        self.body = body

    def __repr__(self):

        """ docstring??? """

        return (
            "Error for %s: %s %s\n Response body: %s" %
            (self.url, self.errcode, self.errmsg, self.body)
            )


# -----------------------------------------------------------------------------
class FetchURL:

    """ docstring ??? """

    def fetch(self, request, host, path, data, cookie=None, username=None, password=None):

        """ docstring??? """

        import httplib, base64
        http = httplib.HTTPConnection(host)
        # write header
        headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
        if cookie:
            headers["Cookie"] = cookie
        # auth
        if username:
            base64string =  base64.encodestring("%s:%s" % (username, password))[:-1]
            authheader =  "Basic %s" % base64string
            headers["Authorization"] = authheader
        http.request(request, path, data, headers)
        # get response
        response = http.getresponse()
        retcode = response.status
        retmsg = response.reason
        retbody = None
        if retcode != 200:
            try:
                retbody = response.read()
            except:
                retbody = None
            raise Error(str(host) + str(path), retcode, retmsg, headers, retbody)
        return response.read()


# -----------------------------------------------------------------------------
@auth.requires_login()
def now():

    """ Manual synchronization """

    import urllib, urlparse
    import gluon.contrib.simplejson as json

    final_status = ""
    sync_start = False
    if "start" in request.args:
        sync_start = True

    # retrieve sync now state
    state = db().select(db.sync_now.ALL, limitby=(0, 1)).first()

    if sync_start:

        # retrieve sync partners from DB
        peers = db().select(db.sync_partner.ALL)

        # retrieve all scheduled jobs set to run manually
        job = db.sync_schedule
        jobs = db((job.period == "m") & (job.enabled == True)).select(job.ALL)
        if not jobs:
            final_status = "There are no scheduled jobs. Please schedule a sync operation (set to run manually).<br /><br /><a href=\"" + URL(r=request, c="sync", f="schedule") + "\">Click here</a> to go to Sync Schedules page.<br /><br />\n"
            return dict(module_name=module_name,
                        sync_status=final_status,
                        sync_start=False,
                        sync_state=state)

        # retrieve settings
        settings = db().select(db.sync_setting.ALL, limitby=(0, 1)).first()

        # url fetcher
        fetcher = FetchURL()

        final_status = ""

        # Find primay resources
        modules = deployment_settings.modules
        tables = []
        for t in db.tables:
            table = db[t]
            if t.find("_") == -1:
                continue
            prefix, name = t.split("_", 1)
            if prefix in modules and \
               "modified_on" in table.fields and \
               "uuid" in table.fields:
                is_component = False
                hook = s3xrc.model.components.get(name, None)
                if hook:
                    link = hook.get("_component", None)
                    if link and link.tablename == t:
                        continue
                    for h in hook.values():
                        if isinstance(h, dict):
                            link = h.get("_component", None)
                            if link and link.tablename == t:
                                is_component = True
                                break
                    if is_component:
                        continue
                # No component
                tables.append(t)

        if not state:
            # New now-job
            res_list = tables # Tables to sync
            first_job = None
            job_cmd = None
            res_list = []
            if jobs:
                first_job = jobs[0]
            if first_job:
                if first_job.job_type == 1:
                    job_cmd = json.loads(first_job.job_command)
                    res_list = job_cmd["resources"]
            # begin new sync now session
            sync_jobs_enabled_list = []
            for job in jobs:
                if job.enabled:
                    sync_jobs_enabled_list.append(str(job.id))
            sync_now_id = db["sync_now"].insert(
                sync_jobs = ", ".join(map(str, sync_jobs_enabled_list)),
                started_on = request.utcnow,
                job_resources_done = "",
                job_resources_pending = ", ".join(map(str, res_list)),
                job_sync_errors = ""
            )
            state = db(db.sync_now.id == sync_now_id).select(db.sync_now.ALL, limitby=(0, 1)).first()
            final_status += "Sync Now started:<br /><br /><br />\n"
        else:
            # Now already started
            sync_now_id = state.id
            final_status += "Sync Now resumed (originally started on " + state.started_on.strftime("%x %H:%M:%S")+ "):<br /><br /><br />\n"

        # unlock session - what for?
        session._unlock(response)
        # become super-user - what for?
        session.s3.roles.append(1)

        # get job from queue
        sync_jobs_list = state.sync_jobs.split(", ")
        if "" in sync_jobs_list:
            sync_jobs_list.remove("")
        sync_job = db(db.sync_schedule.id == int(sync_jobs_list[0])).select(db.sync_schedule.ALL, limitby=(0, 1)).first()
        job_cmd = None
        if sync_job:
            job_cmd = json.loads(sync_job.job_command)
        sync_job_partner = job_cmd["partner_uuid"]
        peer = db(db.sync_partner.uuid == sync_job_partner).select(limitby=(0, 1)).first()

        # Whether a push was successful
        push_success = False

        if sync_job and peer:

            final_status += "<br />Syncing with: " + \
                            peer.name + ", " + \
                            peer.instance_url + \
                            " (" + peer.instance_type + "):<br />\n\n"

            peer_sync_success = True
            last_sync_on = sync_job.last_run
            complete_sync = False
            sync_mode = 1
            if "complete" in job_cmd and str(job_cmd["complete"]) == "True":
                complete_sync = True
            if "policy" in job_cmd:
                sync_policy = int(job_cmd["policy"])
            if "mode" in job_cmd:
                sync_mode = int(job_cmd["mode"])

            sync_resources = []
            sync_errors = ""

            # Keep Session for local URLs
            cookie = str(response.session_id_name) + "=" + str(response.session_id)
            if sync_job.job_type == 1: # Eden <-> Eden

                # Sahana Eden sync

                # Build msince-query
                if last_sync_on is not None and complete_sync == False:
                    msince = last_sync_on.strftime("%Y-%m-%dT%H:%M:%SZ")
                    msince_query = "?msince=" + msince
                else:
                    msince = ""
                    msince_query = ""

                # Find resources to sync
                tablenames = state.job_resources_pending.split(",")

                #for _module, _resource in job_res_list:
                for tablename in tablenames:

                    if tablename not in db.tables:
                        continue

                    if tablename.find("||") == -1:
                        if tablename.find("_") == -1:
                            continue
                        else:
                            prefix, name = tablename.split("_", 1)
                    else:
                        prefix, name = tablename.split("||", 1)

                    resource = s3xrc.resource(prefix, name)

                    sync_path = "sync/sync/%s/%s.%s" % (prefix, name, import_export_format)

                    remote_url = urlparse.urlparse(peer.instance_url)
                    if remote_url.path[-1] != "/":
                        remote_path = "%s/%s" % (remote_url.path, sync_path)
                    else:
                        remote_path = "%s%s" % (remote_url.path, sync_path)

                    if sync_mode in [1, 3]: # pull

                        params = dict(msince=msince)

                        fetch_url = "%s://%s%s?%s" % (remote_url.scheme,
                                                      remote_url.netloc,
                                                      remote_path,
                                                      urllib.urlencode(params))

                        try:
                            result = resource.fetch_xml(fetch_url,
                                                        username=peer.username,
                                                        password=peer.password)
                                                        #proxy=settings.proxy)
                        except SyntaxError, e:
                            result = str(e)

                        try:
                            result_json = json.loads(result)
                        except:
                            error = str(result)
                        else:
                            if str(result_json["statuscode"]).startswith("2"):
                                error = None
                            else:
                                error = str(result_json["message"])

                        if error:
                            tablename_error = "%s (pull error)"
                            sync_errors = "%s\n%s" % (sync_errors,
                                          "Error while synchronizing %s: %s" % (tablename, error))
                            sync_resources.append(tablename_error)
                            final_status += error + "<br /><br />\n"
                        else:
                            sync_resources.append(tablename)
                            final_status += ".........processed %s (Pull Sync)<br />\n" % push_url

                    if sync_mode in [2, 3]: # push

                        params = dict(sync_partner_uuid=settings.uuid)

                        push_url = "%s://%s%s?%s" % (remote_url.scheme,
                                                     remote_url.netloc,
                                                     remote_path,
                                                     urllib.urlencode(params))

                        try:
                            result = resource.push_xml(push_url,
                                                       username=peer.username,
                                                       password=peer.password,
                                                       #proxy=settings.proxy,
                                                       msince=msince)
                        except SyntaxError, e:
                            result = str(e)

                        try:
                            result_json = json.loads(result)
                        except:
                            error = str(result)
                        else:
                            if str(result_json["statuscode"]).startswith("2"):
                                error = None
                            else:
                                error = str(result_json["message"])

                        if error:
                            tablename_error = "%s (push error)"
                            sync_errors = "%s\n%s" % (sync_errors,
                                          "Error while synchronizing %s: %s" % (tablename, error))
                            sync_resources.append(tablename_error)
                            final_status += error + "<br /><br />\n"
                        else:
                            sync_resources.append(tablename)
                            final_status += ".........processed %s (Push Sync)<br />\n" % push_url

            else:
                pass

            # update sync now state
            if state.job_resources_done:
                state.job_resources_done += ","
            state.job_resources_done += ",".join(map(str, sync_resources))

            job_res_pending = state.job_resources_pending.split(",")
            if "" in job_res_pending:
                job_res_pending.remove("")

            if sync_job.job_type == 1:
                for tablename in tablenames:
                    job_res_pending.remove(tablename)

            state.job_resources_pending = ",".join(map(str, job_res_pending))
            state.job_sync_errors += sync_errors
            vals = {"job_resources_done": state.job_resources_done,
                    "job_resources_pending": state.job_resources_pending,
                    "job_sync_errors": state.job_sync_errors}
            db(db.sync_now.id == sync_now_id).update(**vals)
            state = db(db.sync_now.id == sync_now_id).select(db.sync_now.ALL, limitby=(0, 1)).first()

            # check if all resources are synced for the current job, i.e. is it done?
            if (not state.job_resources_pending) or sync_job.job_type == 2:
                # job completed, check if there are any more jobs, if not, then sync now completed

                # log sync job
                if sync_mode == 1:
                    sync_method = "Pull"
                elif sync_mode == 2:
                    sync_method = "Push"
                elif sync_mode == 3:
                    sync_method = "Pull-Push"
                log_table_id = db[log_table].insert(
                    partner_uuid = sync_job_partner,
                    timestmp = datetime.datetime.utcnow(),
                    sync_resources = state.job_resources_done,
                    sync_errors = state.job_sync_errors,
                    sync_mode = "online",
                    sync_method = sync_method,
                    complete_sync = complete_sync
                )

                # remove this job from queue and process next
                sync_jobs_list = state.sync_jobs.split(", ")
                if "" in sync_jobs_list:
                    sync_jobs_list.remove("")

                if len(sync_jobs_list) > 0:
                    sync_jobs_list.remove(sync_jobs_list[0])
                    state.sync_jobs = ", ".join(map(str, sync_jobs_list))
                    state.job_resources_done = ""
                    state.job_resources_pending = ""
                    if len(sync_jobs_list) > 0:
                        next_job_sel = db(db.sync_schedule.id == int(state.sync_jobs[0])).select(db.sync_schedule.ALL)
                        if next_job_sel:
                            next_job = next_job_sel[0]
                            if next_job.job_type == 1:
                                next_job_cmd = json.loads(next_job.job_command)
                                state.job_resources_pending = ", ".join(map(str, next_job_cmd["resources"]))
                    state.job_sync_errors = ""
                    vals = {"sync_jobs": state.sync_jobs,
                            "job_resources_done": state.job_resources_done,
                            "job_resources_pending": state.job_resources_pending}

                    db(db.sync_now.id == sync_now_id).update(**vals)
                    state = db(db.sync_now.id == sync_now_id).select(db.sync_now.ALL, limitby=(0, 1)).first()

                # update last_sync_on
                vals = {"last_sync_on": datetime.datetime.utcnow()}
                db(db.sync_partner.id == peer.id).update(**vals)
                vals = {"last_run": datetime.datetime.utcnow()}
                db(db.sync_schedule.id == sync_job.id).update(**vals)

            if not state.sync_jobs:
                # remove sync now session state
                db(db.sync_now.id == sync_now_id).delete()
                # we're done
                final_status += "Sync completed successfully. Logs generated: " + str(A(T("Click here to open log"),_href=URL(r=request, c="sync", f="history"))) + "<br /><br />\n"

    return dict(module_name=module_name,
                sync_status=final_status,
                sync_start=sync_start,
                sync_state=state)


# -----------------------------------------------------------------------------
def sync():

    """ Sync interface

        allows PUT/GET of any resource (universal RESTful controller)

    """

    # @todo: Do not use global variables
    global sync_peer, sync_policy

    import gluon.contrib.simplejson as json

    if len(request.args) < 2:
        # No resource specified
        raise HTTP(501, body=s3xrc.ERROR.BAD_RESOURCE)
    else:
        prefix = request.args.pop(0)
        name = request.args.pop(0)

        if name.find(".") != -1:
            name, extension = name.rsplit(".", 1)
            request.extension = extension

    # Get the sync partner
    peer_uuid = request.vars.get("sync_partner_uuid", None)
    if peer_uuid:
        peer = db.sync_partner
        sync_peer = db(peer.uuid == peer_uuid).select(limitby=(0,1)).first()
        if sync_peer and not sync_policy:
            sync_policy = sync_peer.policy

    # remote push?
    method = request.env.request_method
    if method in ("PUT", "POST"):
        remote_push = True
        # Must be registered partner for push:
        if not sync_peer:
            raise HTTP(501, body=s3xrc.ERROR.NOT_PERMITTED)
    elif method == "GET":
        remote_push = False
    else:
        raise HTTP(501, body=s3xrc.ERROR.BAD_METHOD)

    # Set the sync resolver
    s3xrc.sync_resolve = lambda vector, peer=sync_peer: sync_res(vector, peer)

    def prep(r):
        # Do not allow interactive formats
        if r.representation in ("html", "popup", "aadata"):
            return False
        # Do not allow URL methods
        if r.method:
            return False
        # Neutralize push limit of the resource
        r.resource.push_limit = None
        return True
    response.s3.prep = prep

    def postp(r, output, sync_peer=sync_peer):

        try:
            output_json = Storage(json.loads(output))
        except:
            # No JSON response?
            pass
        else:
            if r.http in ("PUT", "POST"):

                resource = r.resource
                sr = [c.component.tablename for c in resource.components.values()]