import json
import os
from datetime import timedelta, datetime

from peewee import JOIN_LEFT_OUTER

import features
from data.database import (BuildTriggerService, RepositoryBuildTrigger, Repository, Namespace, User,
                           RepositoryBuild, BUILD_PHASE, db_random_func, UseThenDisconnect)
from data.model import (InvalidBuildTriggerException, InvalidRepositoryBuildException,
                        db_transaction, user as user_model, config)

PRESUMED_DEAD_BUILD_AGE = timedelta(days=15)
PHASES_NOT_ALLOWED_TO_CANCEL_FROM = (BUILD_PHASE.PUSHING, BUILD_PHASE.COMPLETE,
                                     BUILD_PHASE.ERROR, BUILD_PHASE.INTERNAL_ERROR)

ARCHIVABLE_BUILD_PHASES = [BUILD_PHASE.COMPLETE, BUILD_PHASE.ERROR, BUILD_PHASE.CANCELLED]


def update_build_trigger(trigger, config, auth_token=None, write_token=None):
  trigger.config = json.dumps(config or {})
  if auth_token is not None:
    trigger.auth_token = auth_token

  if write_token is not None:
    trigger.write_token = write_token

  trigger.save()


def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None, config=None):
  service = BuildTriggerService.get(name=service_name)
  trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
                                          auth_token=auth_token,
                                          connected_user=user,
                                          pull_robot=pull_robot,
                                          config=json.dumps(config or {}))
  return trigger




def get_build_trigger(trigger_uuid):
  try:
    return (RepositoryBuildTrigger
            .select(RepositoryBuildTrigger, BuildTriggerService, Repository, Namespace)
            .join(BuildTriggerService)
            .switch(RepositoryBuildTrigger)
            .join(Repository)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .switch(RepositoryBuildTrigger)
            .join(User)
            .where(RepositoryBuildTrigger.uuid == trigger_uuid)
            .get())
  except RepositoryBuildTrigger.DoesNotExist:
    msg = 'No build trigger with uuid: %s' % trigger_uuid
    raise InvalidBuildTriggerException(msg)


def list_build_triggers(namespace_name, repository_name):
  return (RepositoryBuildTrigger
          .select(RepositoryBuildTrigger, BuildTriggerService, Repository)
          .join(BuildTriggerService)
          .switch(RepositoryBuildTrigger)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == namespace_name, Repository.name == repository_name))


def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
                        limit):
  return (list_repository_builds(namespace_name, repository_name, limit)
          .where(RepositoryBuildTrigger.uuid == trigger_uuid))


def get_repository_for_resource(resource_key):
  try:
    return (Repository
            .select(Repository, Namespace)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .switch(Repository)
            .join(RepositoryBuild)
            .where(RepositoryBuild.resource_key == resource_key)
            .get())
  except Repository.DoesNotExist:
    return None


def _get_build_base_query():
  return (RepositoryBuild
          .select(RepositoryBuild, RepositoryBuildTrigger, BuildTriggerService, Repository,
                  Namespace, User)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .switch(RepositoryBuild)
          .join(User, JOIN_LEFT_OUTER)
          .switch(RepositoryBuild)
          .join(RepositoryBuildTrigger, JOIN_LEFT_OUTER)
          .join(BuildTriggerService, JOIN_LEFT_OUTER)
          .order_by(RepositoryBuild.started.desc()))


def get_repository_build(build_uuid):
  try:
    return _get_build_base_query().where(RepositoryBuild.uuid == build_uuid).get()

  except RepositoryBuild.DoesNotExist:
    msg = 'Unable to locate a build by id: %s' % build_uuid
    raise InvalidRepositoryBuildException(msg)


def list_repository_builds(namespace_name, repository_name, limit,
                           include_inactive=True, since=None):
  query = (_get_build_base_query()
           .where(Repository.name == repository_name, Namespace.username == namespace_name)
           .limit(limit))

  if since is not None:
    query = query.where(RepositoryBuild.started >= since)

  if not include_inactive:
    query = query.where(RepositoryBuild.phase != BUILD_PHASE.ERROR,
                        RepositoryBuild.phase != BUILD_PHASE.COMPLETE)

  return query


def get_recent_repository_build(namespace_name, repository_name):
  query = list_repository_builds(namespace_name, repository_name, 1)
  try:
    return query.get()
  except RepositoryBuild.DoesNotExist:
    return None


def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
                            display_name, trigger=None, pull_robot_name=None):
  pull_robot = None
  if pull_robot_name:
    pull_robot = user_model.lookup_robot(pull_robot_name)

  return RepositoryBuild.create(repository=repo, access_token=access_token,
                                job_config=json.dumps(job_config_obj),
                                display_name=display_name, trigger=trigger,
                                resource_key=dockerfile_id,
                                pull_robot=pull_robot)


def get_pull_robot_name(trigger):
  if not trigger.pull_robot:
    return None

  return trigger.pull_robot.username


def _get_build_row(build_uuid):
  return RepositoryBuild.select().where(RepositoryBuild.uuid == build_uuid).get()


def update_phase_then_close(build_uuid, phase):
  """ A function to change the phase of a build """
  with UseThenDisconnect(config.app_config):
    try:
      build = _get_build_row(build_uuid)
    except RepositoryBuild.DoesNotExist:
      return False

    # Can't update a cancelled build
    if build.phase == BUILD_PHASE.CANCELLED:
      return False

    updated = (RepositoryBuild
               .update(phase=phase)
               .where(RepositoryBuild.id == build.id, RepositoryBuild.phase == build.phase)
               .execute())
    
    return updated > 0


def create_cancel_build_in_queue(build_phase, build_queue_id, build_queue):
  """ A function to cancel a build before it leaves the queue """

  def cancel_build():
    cancelled = False

    if build_queue_id is not None:
      cancelled = build_queue.cancel(build_queue_id)

    if build_phase != BUILD_PHASE.WAITING:
      return False

    return cancelled

  return cancel_build


def create_cancel_build_in_manager(build_phase, build_uuid, build_canceller):
  """ A function to cancel the build before it starts to push """

  def cancel_build():
    if build_phase in PHASES_NOT_ALLOWED_TO_CANCEL_FROM:
      return False

    return build_canceller.try_cancel_build(build_uuid)

  return cancel_build


def cancel_repository_build(build, build_queue):
  """ This tries to cancel the build returns true if request is successful false
      if it can't be cancelled """
  from app import build_canceller
  from buildman.jobutil.buildjob import BuildJobNotifier

  cancel_builds = [create_cancel_build_in_queue(build.phase, build.queue_id, build_queue),
                   create_cancel_build_in_manager(build.phase, build.uuid, build_canceller), ]
  for cancelled in cancel_builds:
    if cancelled():
      updated = update_phase_then_close(build.uuid, BUILD_PHASE.CANCELLED)
      if updated:
        BuildJobNotifier(build.uuid).send_notification("build_cancelled")

      return updated

  return False


def get_archivable_build():
  presumed_dead_date = datetime.utcnow() - PRESUMED_DEAD_BUILD_AGE

  candidates = (RepositoryBuild
                .select(RepositoryBuild.id)
                .where((RepositoryBuild.phase << ARCHIVABLE_BUILD_PHASES) |
                       (RepositoryBuild.started < presumed_dead_date),
                       RepositoryBuild.logs_archived == False)
                .limit(50)
                .alias('candidates'))

  try:
    found_id = (RepositoryBuild
                .select(candidates.c.id)
                .from_(candidates)
                .order_by(db_random_func())
                .get())
    return RepositoryBuild.get(id=found_id)
  except RepositoryBuild.DoesNotExist:
    return None


def mark_build_archived(build_uuid):
  """ Mark a build as archived, and return True if we were the ones who actually
      updated the row. """
  return (RepositoryBuild
          .update(logs_archived=True)
          .where(RepositoryBuild.uuid == build_uuid,
                  RepositoryBuild.logs_archived == False)
          .execute()) > 0