#!/usr/bin/python -u

import errno
import fcntl
import logging
import os
import socket
import sys
import traceback

try:
    import autotest.common as common  # pylint: disable=W0611
except ImportError:
    import common  # pylint: disable=W0611
from autotest.client.shared import mail, pidfile
from autotest.client.shared import logging_manager, logging_config
from autotest.tko import status_lib, models, dbutils
from autotest.client.shared import utils
from autotest.frontend import optparser
from autotest.frontend import setup_django_environment  # pylint: disable=W0611
from autotest.frontend.tko import models_utils as tko_models_utils


class ParseLoggingConfig(logging_config.LoggingConfig):

    """
    Used with the sole purpose of providing convenient logging setup
    for this program.
    """

    def configure_logging(self, results_dir=None, verbose=False):
        super(ParseLoggingConfig, self).configure_logging(use_console=True,
                                                          verbose=verbose)


class OptionParser(optparser.OptionParser):

    def __init__(self):
        optparser.OptionParser.__init__(self)

        self.add_option("-m", help="Send mail for FAILED tests",
                        dest="mailit", action="store_true")
        self.add_option("-r", help="Reparse the results of a job",
                        dest="reparse", action="store_true")
        self.add_option("-o", help="Parse a single results directory",
                        dest="singledir", action="store_true")
        self.add_option("-l", help=("Levels of subdirectories to include "
                                    "in the job name"),
                        type="int", dest="level", default=1)
        self.add_option("-n", help="No blocking on an existing parse",
                        dest="noblock", action="store_true")

        self.add_option("--write-pidfile",
                        help="write pidfile (.parser_execute)",
                        dest="write_pidfile", action="store_true",
                        default=False)


def parse_args():
    # build up our options parser and parse sys.argv
    parser = OptionParser()
    options, args = parser.parse_args()

    # we need a results directory
    if len(args) == 0:
        logging.error("At least one results directory must be provided")
        parser.print_help()
        sys.exit(1)

    # pass the options back
    return options, args


def format_failure_message(jobname, kernel, testname, status, reason):
    format_string = "%-12s %-20s %-12s %-10s %s"
    return format_string % (jobname, kernel, testname, status, reason)


def mailfailure(jobname, job, message):
    message_lines = [""]
    message_lines.append("The following tests FAILED for this job")
    message_lines.append("http://%s/results/%s" %
                         (socket.gethostname(), jobname))
    message_lines.append("")
    message_lines.append(format_failure_message("Job name", "Kernel",
                                                "Test name", "FAIL/WARN",
                                                "Failure reason"))
    message_lines.append(format_failure_message("=" * 8, "=" * 6, "=" * 8,
                                                "=" * 8, "=" * 14))
    message_header = "\n".join(message_lines)

    subject = "AUTOTEST: FAILED tests from job %s" % jobname
    mail.send("", job.user, "", subject, message_header + message)


def parse_one(jobname, path, reparse, mail_on_failure):
    """
    Parse a single job. Optionally send email on failure.
    """
    logging.info("Scanning %s (%s)", jobname, path)
    old_job_idx = tko_models_utils.job_get_idx_by_tag(jobname)
    # old tests is a dict from tuple (test_name, subdir) to test_idx
    old_tests = {}
    if old_job_idx is not None:
        if not reparse:
            logging.info("Job is already parsed, done")
            return

        old_tests_objs = tko_models_utils.tests_get_by_job_idx(old_job_idx)
        if old_tests_objs:
            old_tests = dict(((test.test, test.subdir), test.test_idx)
                             for test in old_tests_objs)

    # look up the status version
    job_keyval = models.job.read_keyval(path)
    status_version = job_keyval.get("status_version", 0)

    # parse out the job
    parser = status_lib.parser(status_version)
    job = parser.make_job(path)
    status_log = os.path.join(path, "status.log")
    if not os.path.exists(status_log):
        status_log = os.path.join(path, "status")
    if not os.path.exists(status_log):
        logging.error("Unable to parse job, no status file")
        return

    # parse the status logs
    logging.info("Parsing dir=%s, jobname=%s", path, jobname)
    status_lines = open(status_log).readlines()
    parser.start(job)
    tests = parser.end(status_lines)

    # parser.end can return the same object multiple times, so filter out dups
    job.tests = []
    already_added = set()
    for test in tests:
        if test not in already_added:
            already_added.add(test)
            job.tests.append(test)

    # try and port test_idx over from the old tests, but if old tests stop
    # matching up with new ones just give up
    if reparse and old_job_idx is not None:
        job.index = old_job_idx
        for test in job.tests:
            test_idx = old_tests.pop((test.testname, test.subdir), None)
            if test_idx is not None:
                test.test_idx = test_idx
            else:
                logging.info("Reparse returned new test testname=%r subdir=%r",
                             test.testname, test.subdir)
        for test_idx in old_tests.itervalues():
            tko_models_utils.test_delete_by_idx(test_idx)

    # check for failures
    message_lines = [""]
    for test in job.tests:
        if not test.subdir:
            continue
        logging.info("testname, status, reason: %s %s %s",
                     test.subdir, test.status, test.reason)
        if test.status in ("FAIL", "WARN"):
            message_lines.append(format_failure_message(
                jobname, test.kernel.base, test.subdir,
                test.status, test.reason))
    message = "\n".join(message_lines)

    # send out a email report of failure
    if len(message) > 2 and mail_on_failure:
        logging.info("Sending email report of failure on %s to %s",
                     jobname, job.user)
        mailfailure(jobname, job, message)

    dbutils.insert_job(jobname, job)

    # Serializing job into a binary file
    try:
        from autotest.tko import tko_pb2
        from autotest.tko import job_serializer

        serializer = job_serializer.JobSerializer()
        binary_file_name = os.path.join(path, "job.serialize")
        serializer.serialize_to_binary(job, jobname, binary_file_name)

        if reparse:
            site_export_file = "autotest.tko.site_export"
            site_export = utils.import_site_function(__file__,
                                                     site_export_file,
                                                     "site_export",
                                                     _site_export_dummy)
            site_export(binary_file_name)

    except ImportError:
        logging.debug("tko_pb2.py doesn't exist. Create it by compiling "
                      "tko/tko.proto.")


def _site_export_dummy(binary_file_name):
    pass


def _get_job_subdirs(path):
    """
    Returns a list of job subdirectories at path. Returns None if the test
    is itself a job directory. Does not recurse into the subdirs.
    """
    # if there's a .machines file, use it to get the subdirs
    machine_list = os.path.join(path, ".machines")
    if os.path.exists(machine_list):
        subdirs = set(line.strip() for line in file(machine_list))
        existing_subdirs = set(subdir for subdir in subdirs
                               if os.path.exists(os.path.join(path, subdir)))
        if len(existing_subdirs) != 0:
            return existing_subdirs

    # if this dir contains ONLY subdirectories, return them
    contents = set(os.listdir(path))
    contents.discard(".parse.lock")
    subdirs = set(sub for sub in contents if
                  os.path.isdir(os.path.join(path, sub)))
    if len(contents) == len(subdirs) != 0:
        return subdirs

    # this is a job directory, or something else we don't understand
    return None


def parse_leaf_path(path, level, reparse, mail_on_failure):
    job_elements = path.split("/")[-level:]
    jobname = "/".join(job_elements)
    try:
        parse_one(jobname, path, reparse, mail_on_failure)
    except Exception:
        traceback.print_exc()


def parse_path(path, level, reparse, mail_on_failure):
    job_subdirs = _get_job_subdirs(path)
    if job_subdirs is not None:
        # parse status.log in current directory, if it exists. multi-machine
        # synchronous server side tests record output in this directory. without
        # this check, we do not parse these results.
        if os.path.exists(os.path.join(path, 'status.log')):
            parse_leaf_path(path, level, reparse, mail_on_failure)
        # multi-machine job
        for subdir in job_subdirs:
            jobpath = os.path.join(path, subdir)
            parse_path(jobpath, level + 1, reparse, mail_on_failure)
    else:
        # single machine job
        parse_leaf_path(path, level, reparse, mail_on_failure)


def main():
    logging_manager.configure_logging(ParseLoggingConfig(), verbose=True)
    options, args = parse_args()
    results_dir = os.path.abspath(args[0])
    assert os.path.exists(results_dir)

    pid_file_manager = pidfile.PidFileManager("parser", results_dir)

    if options.write_pidfile:
        pid_file_manager.open_file()

    try:
        # build up the list of job dirs to parse
        if options.singledir:
            jobs_list = [results_dir]
        else:
            jobs_list = [os.path.join(results_dir, subdir)
                         for subdir in os.listdir(results_dir)]

        # parse all the jobs
        for path in jobs_list:
            lockfile = open(os.path.join(path, ".parse.lock"), "w")
            flags = fcntl.LOCK_EX
            if options.noblock:
                flags |= fcntl.LOCK_NB
            try:
                fcntl.flock(lockfile, flags)
            except IOError, e:
                # lock is not available and nonblock has been requested
                if e.errno == errno.EWOULDBLOCK:
                    lockfile.close()
                    continue
                else:
                    raise  # something unexpected happened
            try:
                parse_path(path, options.level, options.reparse,
                           options.mailit)

            finally:
                fcntl.flock(lockfile, fcntl.LOCK_UN)
                lockfile.close()

    except Exception:
        pid_file_manager.close_file(1)
        raise
    else:
        pid_file_manager.close_file(0)


if __name__ == "__main__":
    main()
