import os, sys
import logging
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from base64 import b64encode, b64decode
import urllib.parse

from harvest.drivers.s3_driver import S3Driver
from harvest.utils.excel_doc_generator import ExcelDocGenerator
from harvest.exception import TemplateNotFoundError, TemplateMaxCountExceededError, TemplateTypeInvalidError, DuplicateTemplateNameExistsError, NeedsPropertyNotExistsError, PageBreaksCountInvalidError, SettingPropertyCountInvalidError, PrintAreaNotExistsError, SettingPropertyPositionInvalidError

REPOSITORY_BUCKET_NAME="product-harvest-repository"

BASE_TEMPLATES_PATH="templates/excel/"
DEFAULT_TEMPLATES_PATH=BASE_TEMPLATES_PATH + "defaults/"
USERS_TEMPLATES_PATH=BASE_TEMPLATES_PATH + "users/"
PROJECTS_TEMPLATES_PATH=BASE_TEMPLATES_PATH + "projects/"

BASE_LOCAL_WORKSPACE_PATH="/tmp"
LOCAL_WORKSPACE_PATH = os.path.join(BASE_LOCAL_WORKSPACE_PATH, str(uuid4()))

class TemplateController():

  logger = logging.getLogger(__name__)
  logger.addHandler(logging.NullHandler())
  logger.setLevel(logging.DEBUG)

  user_id = None
  project_id = None
  template_id = None

  def __init__(self):
    self.__repo = S3Driver(REPOSITORY_BUCKET_NAME)

    self.logger.info("REPOSITORY_BUCKET_NAME: {}".format(REPOSITORY_BUCKET_NAME))
    self.logger.info("DEFAULT_TEMPLATES_PATH: {}".format(DEFAULT_TEMPLATES_PATH))
    self.logger.info("USERS_TEMPLATES_PATH: {}".format(USERS_TEMPLATES_PATH))
    self.logger.info("PROJECTS_TEMPLATES_PATH: {}".format(PROJECTS_TEMPLATES_PATH))
    self.logger.info("LOCAL_WORKSPACE_PATH: {}".format(LOCAL_WORKSPACE_PATH))

  def set_user_id(self, user_id):
    self.user_id = user_id

  def set_project_id(self, project_id):
    self.project_id = project_id

  def get_template_path(self, template_id):
    # バケットを組み合わせたオブジェクトへ対するパスを生成
    if self.user_id:
      user_tmpls = list_user_templates(self.user_id)
      if template_id in [ tmpl["Key"] for tmpl in user_tmpls ]:
        return USERS_TEMPLATES_PATH + template_id

    if self.project_id:
      project_tmpls = list_project_templates(self.project_id)
      if template_id in [ tmpl["Key"] for tmpl in user_tmpls ]:
        return PROJECTS_TEMPLATES_PATH + template_id

    default_tmpls = list_default_templates()
    if template_id in [ tmpl["Key"] for tmpl in user_tmpls ]:
      return DEFAULT_TEMPLATES_PATH + template_id
    else:
      raise TemplateNotFoundError

  def get(self, template_id, needs_download_url=False):
    # template_idからprefixを生成
    template_path = self.get_template_path(template_id)

    # s3からオブジェクト取得
    # TODO: bytesで返すことも考慮？
    local_template_path = os.path.join(LOCAL_WORKSPACE_PATH, template_id)

    if needs_download_url:
      ret = self.__repo.create_presigned_url(template_path)
      self.logger.info("generated download url: {}".format(ret))

    else:
      self.__repo.download_obj(template_path, local_template_path)
      if not os.path.exists(local_template_path):
        raise Exception
      self.logger.info("template download done")
      ret = local_template_path

    return ret

  def list(self, user_id=None, project_id=None):
    if not user_id:
      user_id = self.user_id
    if not project_id:
      project_id = self.project_id
    
    default_tmpl = self.list_default_templates()
    ret = {
        "default": default_tmpl
    }
    if user_id:
      user_tmpl = self.list_user_templates(user_id)
      ret.update({"user": user_tmpl})

    if project_id:
      project_tmpl = self.list_project_templates(project_id)
      ret.update({"project": project_tmpl})

    self.logger.info("available use templates: %s" % ret)
    return ret

  def __list_templates(self, template_type, any_id=None):
    self.logger.info("needs listup templates type: %s" % template_type)
    if template_type is "default":
      path = DEFAULT_TEMPLATES_PATH
    elif template_type is "user" and any_id:
      path = USERS_TEMPLATES_PATH + any_id
    elif template_type is "project" and any_id:
      path = PROJECTS_TEMPLATES_PATH + any_id
    else:
      raise TemplateTypeInvalidError
    self.logger.info("remote templates path for listup: %s" % path)

    ret = []
    objs = self.__repo.list_objs(path)
    template_names = [ obj["Key"] for obj in objs if obj["Key"] != DEFAULT_TEMPLATES_PATH ]
    self.logger.info("processing template names: %s, fetch tags based on template name" % template_names)
    for name in template_names:
      self.logger.info("processing template name: %s" % name)

      tags = self.__repo.get_obj_tags(name)
      self.logger.info("fetched this template tags: %s" % str(tags))
      tags.update({"template_id": name.split("/")[-1]})

      for key in ["name", "description"]:
        tags[key] = b64decode(tags[key]).decode("utf-8")
      ret.append(tags)
      self.logger.info("fetch and convert processing done. conclusive tags: %s" % str(tags))
    return ret

  def list_default_templates(self):
    return self.__list_templates("default")

  def list_user_templates(self, user_id):
    return self.__list_templates("user", user_id)

  def list_project_templates(self, project_id):
    #path = PROJECTS_TEMPLATES_PATH + project_id
    #ret = self.__repo.list_objs(path)
    #return [ obj["Key"].split("/")[-1] for obj in ret if obj["Key"] != DEFAULT_TEMPLATES_PATH ]
    return self.__list_templates("project", project_id)


  def validate(self, template_path, property_title, property_hierarchy, property_date):
    # webappでのアップロード時に付けられたテンプレートオプションが本当にそのテンプレートで使えるのかを検証する (orderをベースにする？)
    # $hierarchyがあるか？とか$dateがあるか？みたいな
    # validate失敗なら例外を返してwebapp側でその旨をユーザーに伝える必要がある
    # validate成功ならs3のobject-tagとして付与して保存 (保存自体は別のメソッドが担当するが)
    self.logger.info("template validation processing start.")

    excel = ExcelDocGenerator(template_path)
    if not excel.has_name():
      raise NeedsPropertyNotExistsError("{name}") 
    if not excel.has_photo():
      raise NeedsPropertyNotExistsError("{photo}") 
    if not excel.has_type():
      raise NeedsPropertyNotExistsError("{type}") 
    self.logger.info("required property valid.")

    if property_title and not excel.has_title():
      raise NeedsPropertyNotExistsError("{title}") 
    self.logger.info("property_title valid.")

    if property_hierarchy and not excel.has_hierarchy():
      raise NeedsPropertyNotExistsError("{hierarchy}")
    self.logger.info("property_hierarchy valid.")

    if property_date and not excel.has_date():
      raise NeedsPropertyNotExistsError("{date}")
    self.logger.info("property_date valid.")

    page_breaks = excel.get_page_breaks(row_num_only=True)
    if len(page_breaks) not in [0, 1]:
      raise PageBreaksCountInvalidError
    self.logger.info("page breaks valid.")

    if not excel.get_print_area():
      raise PrintAreaNotExistsError
    self.logger.info("print area valid.")

    aggregate_cells = excel.aggregate_cell_values(2)
    for key, values in aggregate_cells.items():
      if len(values) != 1:
        raise SettingPropertyCountInvalidError(key, len(values))
      for cell in values:
        if not excel.has_cell(
            cell=cell, 
            min_row=excel.get_print_area_first_row(), 
            max_row=excel.get_print_area_last_row()
        ):
          raise SettingPropertyPositionInvalidError

        if not cell.value == "{title}" and len(page_breaks) == 1:
          if not excel.has_cell(
              cell=cell, 
              min_row=page_breaks[0] + 1, 
              max_row=excel.get_print_area_last_row()
          ):
            raise SettingPropertyPositionInvalidError
    self.logger.info("setting propery valid.")

    self.logger.info("validation successfully.")
    return True

  def create(
      self, 
      template_type, 
      template_data, 
      parent_id, 
      name, 
      property_title,
      property_hierarchy,
      property_date,
      description=None,
      base64enc=False):

    new_id = str(uuid4())
    self.logger.info("specified template properies: \
        template_type=%s, \
        parent_id=%s, \
        name=%s, \
        property_title=%s, \
        property_hierarchy=%s, \
        property_date=%s, \
        description=%s" % (template_type, parent_id, name, property_title, property_hierarchy, property_date, description))

    if template_type == "user":
      upload_path = "%s%s/%s" % (USERS_TEMPLATES_PATH, parent_id, new_id)
      exists_templates = self.list_user_templates(self.user_id)
    elif template_type == "project":
      upload_path = "%s%s/%s" % (PROJECTS_TEMPLATES_PATH, parent_id, new_id)
      exists_templates = self.list_project_templates(self.project_id)
    else:
      raise TemplateTypeInvalidError
    self.logger.info("template upload path: %s" % upload_path)
    self.logger.info("exists templates: %s" % str(exists_templates))

    # TODO: プロジェクト側の最大数制限の処理も必要
    if len(exists_templates) >= 5:
      raise TemplateMaxCountExceededError
    self.logger.info("has template count validation ok.")

    if name in [ template["name"] for template in exists_templates ]:
      raise DuplicateTemplateNameExistsError
    self.logger.info("duplication names validation ok.")

    # start validate and upload process
    if not os.path.exists(LOCAL_WORKSPACE_PATH):
      os.mkdir(LOCAL_WORKSPACE_PATH)

    local_template_path = os.path.join(LOCAL_WORKSPACE_PATH, new_id + ".xlsx")
    self.logger.info("template temporary stored path: %s" % local_template_path)

    if base64enc:
      template_data = b64decode(template_data)
    with open(local_template_path, mode='w+b') as f:
      f.write(template_data)

    # 指定されたプロパティを本当に使えるかどうかを確認
    if not self.validate(local_template_path, property_title, property_hierarchy, property_date):
      raise NeedsPropertyNotExistsError
    self.logger.info("needs priperties validation ok.")

    tags = self.gen_tags(name, self.user_id, property_title, property_hierarchy, property_date, description)
    self.logger.info("generated tags: %s" % str(tags))
    # TODO: このテンプレートが何のオプションを使える状態なのか？をメタデータとしてタグに付与してアップロード
    # project_idかuser_idで区切ったバケットに対して保存させる(それぞれ5個まで)

    try:
      ret = self.__repo.put_obj(
          upload_path, 
          template_data, 
          tags
      )
    except:
      raise Exception

    return { "message": "successfully" }

  def delete(self, template_id):
    self.logger.info("requested delete template_id: %s" % template_id)
    objs = self.__repo.list_objs(BASE_TEMPLATES_PATH)
    remote_template_path = ""
    for obj in objs:
      if template_id in obj["Key"]:
        remote_template_path = obj["Key"]
        self.logger.info("delete template path: %s" % remote_template_path)
        break
    else:
      raise TemplateNotFoundError
    return self.__repo.delete_obj(remote_template_path)

  def gen_tags(self, name: str, user_id: str, title: bool, hierarchy: bool, date: bool, description: str = None) -> str:
    # TODO: type(user|project)っているかな？
    # TODO: created_atってなくても困らないよね？updateを許さないならlast_modifyで判断可能だし、作成日時を誰かが必要とするか？という話もあるし
    # TODO: ownerも困らないよね？prefixから判断できるし
    tags = [
  	  "name=%s" % urllib.parse.quote(b64encode(name.encode("utf-8")).decode("utf-8")),
  	  "created_by=%s" % user_id,
      "property_title=%s" % str(title),
      "property_hierarchy=%s" % str(hierarchy),
  	  "property_date=%s" % str(date)
    ]
    if description:
      tags.append("description=%s" % urllib.parse.quote(b64encode(description.encode("utf-8")).decode("utf-8")))
    return "&".join(tags)
