from contextlib import contextmanager
import shutil
import tempfile
from datetime import datetime

from pyparsing import punc8bit

try:
    import arrow
except Exception:
    arrow = None
from collections import OrderedDict
import threading
from . import click
import json
from pathlib import Path
import os
import time
import subprocess
from os.path import expanduser
from .consts import VERSIONS

try:
    import psycopg2
except Exception:
    pass


def get_odoo_addons_paths(
    relative=False, no_extra_addons_paths=False, additional_addons_paths=False
):
    m = MANIFEST()
    c = customs_dir()
    res = []
    addons_paths = m["addons_paths"]
    if additional_addons_paths:
        addons_paths += additional_addons_paths
    for x in addons_paths:
        if no_extra_addons_paths:
            if x not in ["odoo/addons", "odoo/odoo/addons"]:
                continue
        if relative:
            res.append(x)
        else:
            res.append(c / x)
    return res


def _identify_odoo_addons_paths(show_conflicts=True):
    from .module_tools import Module

    folders = []
    c = customs_dir()

    modules = []

    def _get_modules_in_folder(folder):
        # find extra modules without repo
        for file in folder.glob("**/{}".format(MANIFEST_FILE())):
            if ".git" in file.parts:
                continue
            file = file.resolve().absolute()
            if file.parent.parent in folders:
                continue
            if any(x in file.relative_to(c).parts for x in {"test", "tests", ".git"}):
                continue
            try:
                module = Module(file)
            except Module.IsNot:
                continue
            if module.in_version:
                if module.path.parent not in folders:
                    folders.append(module.path.parent)
                    modules.append(module)
            del file

    manifest = MANIFEST()

    oca_addons = [
        "addons_oca",
        "OCA",
        "addons_OCA",
        "*OCA",
    ]
    for oca in oca_addons:
        if oca not in os.listdir(c):
            continue

        for oca_folder in list(c.glob(oca)):  # case insensitive on windows / macos
            _get_modules_in_folder(oca_folder)
            del oca_folder
        del oca

    for module in manifest["modules"]:
        for url in module["urls"]:
            repo_name = url.split("/")[-1].replace(".git", "")
            path = c / Path(module["path"]) / repo_name
            _get_modules_in_folder(path)
            del url
        del module

    folders = list(reversed(folders))
    for odoo_folder in filter(
        lambda x: x.exists(),
        map(
            Path,
            [
                c / "odoo" / "openerp" / "addons",  # backwards compatibility
                c / "odoo" / "addons",
                c / "odoo" / "odoo" / "addons",
            ],
        ),
    ):
        if not odoo_folder.exists():
            continue
        _get_modules_in_folder(odoo_folder)
        del odoo_folder
    folders = list(reversed(folders))
    _get_modules_in_folder(c)

    if show_conflicts:
        _detect_duplicate_modules(folders, modules)

    return folders


def _detect_duplicate_modules(folders, modules):
    from .module_tools import Module

    check_modules = {}
    c = customs_dir()
    for folder in folders:
        folder_rel = folder.relative_to(c)
        modules_in_folder = [
            x for x in modules if str(x.path.relative_to(c)).startswith(str(folder_rel))
        ]
        for module in modules_in_folder:
            check_modules.setdefault(module.name, [])
            if module.path not in [x.path for x in check_modules[module.name]]:
                check_modules[module.name].append(module)

    for v in filter(lambda x: len(x) > 1, check_modules.values()):
        click.echo(
            click.style(
                "Overridden Module: {}".format(v[0].name), bold=True, fg="green"
            )
        )
        for i, module in enumerate(v):
            styles = dict(bold=True) if not i else {}
            s = str(module.path.relative_to(c))
            click.echo(click.style(s, **styles))
    time.sleep(2)


def customs_dir():
    env_customs_dir = os.getenv("CUSTOMS_DIR")
    if not env_customs_dir:
        manifest_file = Path(os.getcwd()) / "MANIFEST"
        if manifest_file.exists():
            return manifest_file.parent
        else:
            here = Path(os.getcwd())
            while not (here / "MANIFEST").exists():
                here = here.parent
                if here.parent == here:
                    break
            if (here / "MANIFEST").exists():
                return here

            click.secho("no MANIFEST file found in current directory.")
    if not env_customs_dir:
        return None
    return Path(env_customs_dir)


def plaintextfile():
    path = customs_dir() / ".odoo.ast"
    return path


def _read_file(path, default=None):
    try:
        with open(path, "r") as f:
            return (f.read() or "").strip()
    except Exception:
        return default


def MANIFEST_FILE():
    _customs_dir = customs_dir()
    if not _customs_dir:
        return None
    return _customs_dir.resolve().absolute() / "MANIFEST"


class MANIFEST_CLASS(object):
    def __init__(self):
        self.path = MANIFEST_FILE()

        self._apply_defaults()

    def _apply_defaults(self):
        d = self._get_data()
        d.setdefault("modules", [])
        # patches ?

        self.patch_dir = customs_dir() / "patches"

        if "version" not in d:
            self["version"] = float(d["version"])

    def _get_data(self):
        return OrderedDict(eval(self.path.read_text() or "{}"))

    def __getitem__(self, key):
        data = self._get_data()
        return data[key]

    def get(self, key, default):
        return self._get_data().get(key, default)

    def __setitem__(self, key, value):
        data = self._get_data()
        data[key] = value
        self._update(data)

    def _update(self, d):
        d["install"] = list(sorted(d["install"]))
        s = json.dumps(d, indent=4)
        tfile = Path(tempfile.mktemp(suffix=".MANIFEST"))
        tfile.write_text(s)
        shutil.move(tfile, MANIFEST_FILE())

    def rewrite(self):
        self._update(self._get_data())


def MANIFEST():
    return MANIFEST_CLASS()


def current_version():
    return float(MANIFEST()["version"])


def get_postgres_connection_params(inside_container=None):
    config = get_settings()
    if (
        not inside_container
        and os.getenv("DOCKER_MACHINE") != "1"
        and config.get("RUN_POSTGRES") == "1"
    ):
        host = Path(os.environ["HOST_RUN_DIR"]) / "postgres.socket"
        port = 0

    else:
        host = config["DB_HOST"]
        port = int(config.get("DB_PORT", "5432"))
    password = config["DB_PWD"]
    user = config["DB_USER"]
    return host, port, user, password


def get_settings():
    """
    Can run outside of host and inside host. Returns all values from
    composed settings file.
    """
    from .myconfigparser import MyConfigParser  # NOQA

    if os.getenv("DOCKER_MACHINE") == "1":
        settings_path = Path("/tmp/settings")
        content = ""
        for k, v in os.environ.items():
            content += f"{k}={v}\n"
        settings_path.write_text(content)
    else:
        settings_path = Path(os.environ["HOST_RUN_DIR"]) / "settings"
    myconfig = MyConfigParser(settings_path)
    return myconfig


def get_conn(db=None, host=None):
    config = get_settings()
    if db != "postgres":
        # waiting until postgres is up
        get_conn(db="postgres")

    host, port, user, password = get_postgres_connection_params()
    db = db or config["DBNAME"]
    connstring = "dbname={}".format(db)

    for combi in [
        ("password", password),
        ("host", host),
        ("port", port),
        ("user", user),
    ]:
        if combi[1]:
            connstring += " {}='{}'".format(combi[0], combi[1])

    conn = psycopg2.connect(connstring)
    cr = conn.cursor()
    return conn, cr


@contextmanager
def get_conn_autoclose(*args, **kwargs):
    conn, cr = get_conn(*args, **kwargs)
    try:
        yield cr
    except Exception:
        conn.rollback()
        raise
    else:
        conn.commit()
    finally:
        cr.close()
        conn.close()


def translate_path_into_machine_path(path):
    path = customs_dir() / translate_path_relative_to_customs_root(path)
    return path


def translate_path_relative_to_customs_root(path):
    """
    The customs must contain a significant file named
    MANIFEST to indicate the root of the customs
    """

    cmf = MANIFEST_FILE().absolute().resolve().absolute()
    if not str(path).startswith("/"):
        return path

    try:
        path = path.resolve()
    except Exception:
        pass

    return path.relative_to(cmf.parent)


def manifest_file_names():
    result = "__manifest__.py"
    try:
        current_version()
    except Exception:
        pass
    else:
        if current_version() <= 10.0:
            result = "__openerp__.py"
        else:
            result = "__manifest__.py"
    return result
