import os
import platform
import subprocess
import threading
import binascii

import whatap.agent.conf.configure as config
import whatap.util.logging_util as logging_util


# Patch script names
PATCH_SCRIPT = "infra_agent_patch.sh"
ENV_FILE = "whatap.env"

# Platform detection
_os_type = platform.system()  # 'Linux', 'AIX', 'HP-UX', 'SunOS'

# Agent Patch Error Codes
ERR_SM_AGENT_PATCH_GENERAL            = "ER708000"
ERR_SM_AGENT_PATCH_FILE_NOT_FOUND     = "ER708001"
ERR_SM_AGENT_PATCH_SCRIPT_NOT_EXECUTE = "ER708002"
ERR_SM_AGENT_PATCH_ENV_ERROR          = "ER708003"
ERR_SM_AGENT_PATCH_EXECUTE_ERROR      = "ER708004"

ERR_SM_AGENT_PATCH_STATUS_GENERAL = "ER709000"
ERR_SM_AGENT_PATCH_LOG_READ_FAIL  = "ER709001"
ERR_SM_AGENT_PATCH_LOG_EMPTY      = "ER709002"

# File operation error codes
ERR_SM_FILE_SAVE_ERROR      = "ER705001"
ERR_SM_FILE_SAVE_DONE_ERROR = "ER705002"
ERR_SM_FILE_LIST_ERROR      = "ER706001"
ERR_SM_FILE_DELETE_ERROR    = "ER707001"

# Module-level state
_whatap_dir = ""
_package_dir = ""
_file_save_mutex = threading.Lock()


class AgentPatchError(Exception):
    def __init__(self, code, message, err=None):
        self.code = code
        self.message = message
        self.err = err
        super(AgentPatchError, self).__init__(self._format_message())

    def _format_message(self):
        if self.err:
            return "[{}] {}: {}".format(self.code, self.message, self.err)
        return "[{}] {}".format(self.code, self.message)

    def error_code(self):
        return self.code


class PatchStatus(object):
    def __init__(self, status="", patch_version="", start_time="", end_time="", error_message=""):
        self.status = status
        self.patch_version = patch_version
        self.start_time = start_time
        self.end_time = end_time
        self.error_message = error_message


def init_agent_patch():
    global _whatap_dir, _package_dir

    whatap_home = os.environ.get('WHATAP_HOME', '/usr/whatap/infra/conf')
    _whatap_dir = os.path.dirname(whatap_home)

    conf = config.GetConfig()
    configured_dir = getattr(conf, 'PackageDir', '/tmp/whatap-package')
    if configured_dir:
        _package_dir = configured_dir
    else:
        _package_dir = '/tmp/whatap-package'

    if not os.path.exists(_package_dir):
        try:
            os.makedirs(_package_dir)
        except Exception as e:
            logging_util.error("[AgentPatch] Failed to create package dir: {}".format(e))

    logging_util.info("[AgentPatch] Initialized: whatap_dir={}, package_dir={}".format(
        _whatap_dir, _package_dir))


# ---------------------------------------------------------------------------
# Package file operations (equivalent to Go's PackageSave / BigFileSave)
# ---------------------------------------------------------------------------

def package_save(package_name, pos, data):
    global _package_dir

    if not os.path.exists(_package_dir):
        try:
            os.makedirs(_package_dir)
        except Exception as e:
            raise AgentPatchError(ERR_SM_FILE_SAVE_ERROR,
                                  "failed to create directory: {}".format(_package_dir), e)

    file_path = os.path.join(_package_dir, os.path.basename(package_name))
    tmp_path = file_path + ".tmp"

    try:
        if pos == 0:
            # New file: create or truncate
            if os.path.exists(tmp_path):
                os.remove(tmp_path)
            with open(tmp_path, 'wb') as f:
                f.write(data)
        else:
            # Append: open existing, seek to position
            if not os.path.exists(tmp_path):
                raise AgentPatchError(ERR_SM_FILE_SAVE_ERROR,
                                      "temp file not found for pos > 0", None)
            actual_size = os.path.getsize(tmp_path)
            if actual_size < pos:
                raise AgentPatchError(ERR_SM_FILE_SAVE_ERROR,
                                      "invalid position: {}, size={}".format(pos, actual_size), None)
            with open(tmp_path, 'r+b') as f:
                f.seek(pos)
                f.write(data)

    except AgentPatchError:
        raise
    except Exception as e:
        raise AgentPatchError(ERR_SM_FILE_SAVE_ERROR, "failed to save package chunk", e)


def package_save_done(package_name, size, crc):
    global _package_dir

    crc = crc & 0xFFFFFFFF

    file_path = os.path.join(_package_dir, os.path.basename(package_name))
    tmp_path = file_path + ".tmp"

    try:
        if not os.path.exists(tmp_path):
            raise AgentPatchError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                  "temp file not found: {}".format(tmp_path), None)

        # Verify size
        actual_size = os.path.getsize(tmp_path)
        if actual_size != size:
            os.remove(tmp_path)
            raise AgentPatchError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                  "size mismatch: expected={}, actual={}".format(size, actual_size), None)

        # Verify CRC32
        actual_crc = _get_crc32(tmp_path)
        if crc != 0 and actual_crc != 0 and actual_crc != crc:
            os.remove(tmp_path)
            raise AgentPatchError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                  "crc32 mismatch: expected={}, actual={}".format(crc, actual_crc), None)

        # Atomic rename: tmp -> final
        if os.path.exists(file_path):
            os.remove(file_path)
        os.rename(tmp_path, file_path)

    except AgentPatchError:
        raise
    except Exception as e:
        raise AgentPatchError(ERR_SM_FILE_SAVE_DONE_ERROR, "failed to finalize package save", e)


def _get_crc32(file_path):
    try:
        crc = 0
        with open(file_path, 'rb') as f:
            while True:
                chunk = f.read(8192)
                if not chunk:
                    break
                crc = binascii.crc32(chunk, crc)
        return crc & 0xFFFFFFFF
    except Exception:
        return 0


def package_list():
    global _package_dir
    try:
        if not os.path.exists(_package_dir):
            return [], None

        files = []
        for root, dirs, filenames in os.walk(_package_dir):
            for f in filenames:
                files.append(os.path.join(root, f))
        return files, None

    except Exception as e:
        return [], e


def package_delete(package_name):
    global _package_dir
    file_path = os.path.join(_package_dir, os.path.basename(package_name))
    try:
        if os.path.exists(file_path):
            os.remove(file_path)
        return None
    except Exception as e:
        return e


# ---------------------------------------------------------------------------
# Agent Patch
# ---------------------------------------------------------------------------

def create_env_file(package_path):
    global _whatap_dir
    env_file = os.path.join(_whatap_dir, ENV_FILE)
    content = "export INSTALL_DIR={}\nexport pkg={}\n".format(_whatap_dir, package_path)
    try:
        with open(env_file, 'w') as f:
            f.write(content)
    except Exception as e:
        raise AgentPatchError(ERR_SM_AGENT_PATCH_ENV_ERROR,
                              "Failed to create env file", e)


def agent_patch(package_name):
    global _whatap_dir, _package_dir

    logging_util.debug("[AgentPatch] Starting agent patch process with package: {}".format(package_name))

    # 1. Check package file exists
    package_path = os.path.join(_package_dir, package_name)
    if not os.path.exists(package_path):
        raise AgentPatchError(ERR_SM_AGENT_PATCH_FILE_NOT_FOUND,
                              "Package file not found: {}".format(package_path), None)

    # 2. Check patch script exists
    patch_script_path = os.path.join(_whatap_dir, PATCH_SCRIPT)
    if not os.path.exists(patch_script_path):
        raise AgentPatchError(ERR_SM_AGENT_PATCH_SCRIPT_NOT_EXECUTE,
                              "Patch script not found: {}".format(patch_script_path), None)

    # 3. Check script is executable
    if not os.access(patch_script_path, os.X_OK):
        raise AgentPatchError(ERR_SM_AGENT_PATCH_SCRIPT_NOT_EXECUTE,
                              "Patch script is not executable: {}".format(patch_script_path), None)

    # 4. Create env file (INSTALL_DIR, pkg)
    create_env_file(package_path)

    # 5. Run updater service (detached, Setsid)
    _run_agent_update()

    logging_util.debug("[AgentPatch] Agent patch process completed")


def _run_agent_update():
    try:
        if _os_type == 'AIX':
            # AIX: use SRC (System Resource Controller)
            cmd = ["sh", "-c", "stopsrc -s whatap-updater 2>/dev/null; startsrc -s whatap-updater"]
        elif _os_type == 'HP-UX':
            # HP-UX: use init.d service script
            cmd = ["sh", "/sbin/init.d/whatap-updater", "restart"]
        elif _os_type == 'SunOS':
            # Solaris: use init.d service script
            cmd = ["sh", "/etc/init.d/whatap-updater", "restart"]
        else:
            # Linux (default): use whatap-updater service
            cmd = ["bash", "-c", "service whatap-updater restart"]

        with open(os.devnull, 'w') as devnull:
            subprocess.Popen(
                cmd,
                stdout=devnull,
                stderr=devnull,
                close_fds=True,
                preexec_fn=os.setsid
            )
        logging_util.debug("[AgentPatch] Update service restart: os={}, cmd={}".format(_os_type, cmd))
    except Exception as e:
        raise AgentPatchError(ERR_SM_AGENT_PATCH_EXECUTE_ERROR,
                              "Failed to restart update service", e)


# ---------------------------------------------------------------------------
# Agent Patch Status
# ---------------------------------------------------------------------------

def agent_patch_status():
    global _whatap_dir

    history_file = os.path.join(_whatap_dir, "patch_history.log")

    if not os.path.exists(history_file):
        raise AgentPatchError(ERR_SM_AGENT_PATCH_LOG_READ_FAIL,
                              "patch_history.log not found: {}".format(history_file), None)

    last_line = _get_last_line_safe(history_file)
    if not last_line:
        raise AgentPatchError(ERR_SM_AGENT_PATCH_LOG_EMPTY,
                              "patch_history.log is empty", None)

    # Parse: status|patch_version|start_time|end_time|error_message
    parts = last_line.split("|")
    if len(parts) != 5:
        raise AgentPatchError(ERR_SM_AGENT_PATCH_LOG_READ_FAIL,
                              "Invalid patch history format: {}".format(last_line), None)

    status = PatchStatus(
        status=parts[0],
        patch_version=parts[1],
        start_time=parts[2],
        end_time=parts[3],
        error_message=parts[4]
    )

    logging_util.debug("[AgentPatch] Patch status: status={}, version={}, start={}, end={}, error={}".format(
        status.status, status.patch_version, status.start_time, status.end_time, status.error_message))

    return status


def _get_last_line_safe(file_path):
    try:
        file_size = os.path.getsize(file_path)
        if file_size == 0:
            return ""

        with open(file_path, 'r') as f:
            if file_size <= 4096:
                content = f.read()
            else:
                f.seek(-4096, 2)
                content = f.read()

        return _get_last_line_from_content(content)

    except Exception as e:
        logging_util.error("[AgentPatch] Failed to read last line: {}".format(e))
        return ""


def _get_last_line_from_content(content):
    content = content.rstrip('\r\n')
    if not content:
        return ""

    lines = content.split('\n')
    for i in range(len(lines) - 1, -1, -1):
        line = lines[i].strip()
        if line:
            return line
    return ""
