import time
from datetime import datetime, timezone
import calendar
import smtplib
from email.message import EmailMessage

from collections import defaultdict

epoch = datetime.utcfromtimestamp(0).replace(tzinfo=timezone.utc)


def access_secret_version(project_id, secret_id, version_id):
  """
  Access the payload for the given secret version if one exists. The version
  can be a version number as a string (e.g. "5") or an alias (e.g. "latest").
  """

  # Import the Secret Manager client library.
  from google.cloud import secretmanager

  # Create the Secret Manager client.
  client = secretmanager.SecretManagerServiceClient()

  # Build the resource name of the secret version.
  secret_version_path = f'projects/{project_id}/secrets/{secret_id}/versions/{version_id}'
  #name = client.secret_version_path(project_id, secret_id, version_id)

  # Access the secret version.
  response = client.access_secret_version(request={'name': secret_version_path})
  payload = response.payload.data.decode('UTF-8')
  return payload


def send_email(sender, pw, recipient, subject, message):
  msg = EmailMessage()
  msg.set_content(message)
  msg['Subject'] = subject
  msg['From'] = sender
  msg['To'] = recipient

  server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
  server.login(sender, pw)
  server.send_message(msg)
  server.quit()


def format_date_time(epoch_ms):
  if not epoch_ms:
    return None
  return datetime.utcfromtimestamp(int(epoch_ms) // 1000).replace(microsecond=int(epoch_ms) % 1000 * 1000)


def date_from_epoch_ms(epoch_ms):
  date_time = format_date_time(epoch_ms)
  if not date_time:
    return None
  return date_time.date()


def closest_date_next_month(year, month, day):
  month = month + 1
  if month == 13:
    month = 1
    year = year + 1

  condition = True
  while condition:
    try:
      return datetime(year, month, day)
    except ValueError:
      day = day - 1
    condition = day > 26

  raise Exception('Problem getting date next month')


def pass_job(job, start_time, success, event, error, results, job_history=None):
  if job_history:
    job_history.update(
        start_time=start_time,
        end_time=datetime.now(),
        success=success,
        input=event,
        error=error,
        results=results
    )
  else:
    job_history = app_tables.job_history.add_row(
        job=job,
        start_time=start_time,
        end_time=datetime.now(),
        success=success,
        input=event,
        error=error,
        results=results
    )
  return job_history


def fail_job(job, job_name, user_email, start_time, event, error, results, stack_trace, job_history=None):
  if job_history:
    job_history.update(
        start_time=start_time,
        end_time=datetime.now(),
        success=False,
        input=event,
        error=error,
        results=results
    )
  else:
    job_history = app_tables.job_history.add_row(
        job=job,
        start_time=start_time,
        end_time=datetime.now(),
        success=False,
        input=event,
        error=error,
        results=results
    )
  anvil.server.call('send_admin_error_email', job_name, user_email, start_time, event, error, stack_trace)
  return job_history


def start_admin_job(job_history):
  job_start_time = now()
  job_history.update(last_job_start_time=format_date_time(job_start_time), status="running...")
  return job_start_time


def pass_admin_job(job_history, job_start_time, results):
  job_end_time = now()
  job_history.update(
      last_job_run_results=results,
      last_job_end_time=format_date_time(job_end_time),
      status="completed successfully",
      last_successful_run_time=format_date_time(job_end_time),
      last_job_duration=round((job_end_time - job_start_time) / 1000, 3)
  )


def fail_admin_job(job_history, job_start_time, results, message):
  job_end_time = now()
  job_history.update(
      last_job_run_results=results,
      last_job_end_time=format_date_time(job_end_time),
      status=f"failed: {message}",
      last_job_duration=round((job_end_time - job_start_time) / 1000, 3)
  )


def now():
  return round(time.time() * 1000)


def getEpochMillis(dt):
  return (dt - epoch).total_seconds() * 1000


def validateDateAndReturnEpochTimeInMillis(date):
  date = str(date).replace("-", "/").replace(".", "/")
  yyyy, mm, dd = date.split('/')
  dd = int(dd.lstrip("0"))
  mm = int(mm.lstrip("0"))
  yyyy = int(yyyy)
  if mm in [1, 3, 5, 7, 8, 10, 12]:
    maxDay = 31
  elif mm in [4, 6, 9, 11]:
    maxDay = 30
  elif yyyy % 4 == 0 and yyyy % 100 != 0 or yyyy % 400 == 0:
    maxDay = 29
  else:
    maxDay = 28

  if mm < 1 or mm > 12:
    raise Exception("Please specify a valid month.")
  elif dd < 1 or dd > maxDay:
    raise Exception("Please specify a valid day.")
  elif len(f"{yyyy}") != 4:
    raise Exception("Please specify a valid 4-digit year.")
  else:
    dateTime = f"{date} 00:00:00"
    epoch = int(time.mktime(time.strptime(dateTime, "%Y/%m/%d %H:%M:%S"))) * 1000

  return epoch


def partition(seq, key):
  d = {}
  for x in seq:
    d[key(x)] = []
  return d


def makehash():
  return defaultdict(makehash)


def get_filtered_dict(search_term, dictionary):
  return {k: v for k, v in dictionary.items() if any(search_term in str(x).lower() for x in v.values())}


def is_success(status_code):
  return str(status_code).startswith('2')


def is_not_found(status_code):
  return str(status_code) == '404'


def is_retryable(status_code):
  return str(status_code) in ['502', '429'] or not status_code


def is_unauthorized(status_code):
  return str(status_code) in ['401', '403']


def validate_response(response, error_message, success_message=None):
  if not is_success(response['status_code']):
    response['error_message'] = '%s %s:%s' % (error_message, response['status_code'], response['content'])
  elif success_message != None:
    response['success_message'] = success_message
  return response


def remove_keys_from_dict(d, keys):
  return {k: v for k, v in d.items() if k not in keys}


def remove_keys_from_dict_except(d, keys):
  return {k: v for k, v in d.items() if k in keys}


def apply_func_to_items(iterable, func):
  return map(lambda x: func(x), iterable)
