import logging

from collections import defaultdict
from flask import current_app
from uuid import UUID  # NOQA

from changes.config import db
from changes.constants import ResultSource
from changes.db.utils import create_or_update
from changes.jobs.signals import fire_signal
from changes.lib.build_type import is_any_commit_build
from changes.lib.revision_lib import get_child_revisions, get_latest_finished_build_for_revision
from changes.models.bazeltarget import BazelTarget
from changes.models.build import Build
from changes.models.job import Job
from changes.models.jobplan import JobPlan
from changes.models.project import Project
from changes.models.revision import Revision
from changes.models.revisionresult import RevisionResult
from changes.utils.agg import aggregate_result


logger = logging.getLogger(__name__)


def revision_result_build_finished_handler(build_id, **kwargs):
    build = Build.query.get(build_id)
    if build is None:
        return
    if not is_any_commit_build(build):
        return
    create_or_update_revision_result(
        revision_sha=build.source.revision_sha,
        project_id=build.project_id,
        propagation_limit=current_app.config[
            'SELECTIVE_TESTING_PROPAGATION_LIMIT'],
    )


def create_or_update_revision_result(revision_sha, project_id, propagation_limit):
    """Given a revision sha and project ID, try to update the revision result
    for it. This involves copying results for unaffected Bazel targets from
    the latest parent build.

    `propagation_limit` is used to control how many times this function will
    be called recursively on the revision's children. If it is 0, then this
    function only updates the current revision's revision result and does
    not do any recursion.
    """
    # type: (str, UUID, int) -> None
    project = Project.query.get(project_id)
    revision = Revision.query.filter(
        Revision.sha == revision_sha,
        Revision.repository_id == project.repository_id,
    ).first()
    last_finished_build = get_latest_finished_build_for_revision(
        revision_sha, project_id)
    if not last_finished_build:
        return

    unaffected_targets = BazelTarget.query.join(
        Job, BazelTarget.job_id == Job.id,
    ).filter(
        BazelTarget.result_source == ResultSource.from_parent,
        Job.build_id == last_finished_build.id,
    ).all()

    if len(unaffected_targets) > 0 and len(revision.parents) > 0:
        # TODO(naphat) there's probably a better way to select parent,
        # but that happens rarely enough that it can be punted for now
        parent_revision_sha = revision.parents[0]

        # TODO(naphat) we should find a better way to select parent builds.
        # Even if a parent build is not finished, we can already start to
        # take a look at target results, as it may already have results
        # for all of our unaffected_targets
        # perhaps an optimization is to take the latest build, instead
        # of the latest finished build. Finished build are more likely
        # to have the complete set of targets we need though. But if
        # a finished build is not the latest build, then maybe that
        # finished build had an infra failure. Anyways, for simplicity,
        # let's stick to finished build for now.
        parent_build = get_latest_finished_build_for_revision(
            parent_revision_sha, project_id)
        if parent_build:
            # group unaffected targets by jobs
            unaffected_targets_groups = defaultdict(lambda: {})
            for target in unaffected_targets:
                unaffected_targets_groups[target.job_id][target.name] = target

            # process targets in batch, grouped by job id
            # almost always, this is going to be a single job - there is
            # usually only one autogenerated plan per project.
            for job_id, targets_dict in unaffected_targets_groups.iteritems():
                jobplan = JobPlan.query.filter(
                    JobPlan.project_id == project_id,
                    JobPlan.build_id == last_finished_build.id,
                    JobPlan.job_id == job_id,
                ).first()
                if not jobplan:
                    continue
                parent_targets = BazelTarget.query.join(
                    Job, BazelTarget.job_id == Job.id,
                ).join(
                    JobPlan, BazelTarget.job_id == JobPlan.job_id,
                ).filter(
                    Job.build_id == parent_build.id,
                    BazelTarget.name.in_(targets_dict),
                    JobPlan.plan_id == jobplan.plan_id,
                )
                for parent_target in parent_targets:
                    targets_dict[parent_target.name].result = parent_target.result
                    db.session.add(targets_dict[parent_target.name])
        else:
            logger.info("Revision %s could not find a parent build for parent revision %s.", revision_sha, parent_revision_sha)

    revision_result, _ = create_or_update(RevisionResult, where={
        'revision_sha': revision_sha,
        'project_id': project_id,
    }, values={
        'build_id': last_finished_build.id,
        'result': aggregate_result([last_finished_build.result] + [t.result for t in unaffected_targets]),
    })

    db.session.commit()
    fire_signal.delay(
        signal='revision_result.updated',
        kwargs={'revision_result_id': revision_result.id.hex},
    )

    if propagation_limit > 0:
        # TODO stop the propagation if nothing changed
        for child_revision in get_child_revisions(revision):
            create_or_update_revision_result(
                child_revision.sha, project_id, propagation_limit=propagation_limit - 1)
