# -*- coding: utf-8 -*-
# NEW: __future__ imports for Py2/3 compatibility (print_function for print(..., file=), absolute_import for relative ., unicode_literals for str/bytes handling, division for /)
from __future__ import print_function, absolute_import, division, unicode_literals

# src/chronicle_logger/ChronicleLogger.py
import os
import sys
import ctypes
import tarfile
import re
from datetime import datetime
# NEW: Added import for subprocess to execute pyenv commands in pyenvVenv method
import subprocess


# Correct import for your actual file: Suroot.py (capital S)
from .Suroot import _Suroot

try:
    basestring
except NameError:
    basestring = str

# NEW: io.open fallback for encoding='utf-8' support in Py2 (io.open added in Py2.6; use conditional for safety)
try:
    from io import open as io_open
except ImportError:
    io_open = open

# baseDir should be independent
# It should never be affected by root/sudo/normal user
# It is for cross-application configuration, not logging
# Getting the parent of logDir() is trivial if needed
# We should not couple them
# 
# baseDir()  → /var/myapp        ← user sets this explicitly
#              /home/user/.myenv
#              /opt/myapp
# 
# logDir()   → /var/log/myapp   ← automatically derived only if user is root
#              ~/.app/myapp/log ← if user is non-root (no matter is sudo or not )


class ChronicleLogger:
    CLASSNAME = "ChronicleLogger"
    MAJOR_VERSION = 1
    MINOR_VERSION = 2
    PATCH_VERSION = 1

    LOG_ARCHIVE_DAYS = 7
    LOG_REMOVAL_DAYS = 30

    def __init__(self, logname=b"app", logdir=b"", basedir=b""):
        self.__logname__ = None
        self.__basedir__ = None
        self.__logdir__ = None
        self.__old_logfile_path__ = ctypes.c_char_p(b"")
        self.__is_python__ = None

        if not logname or logname in (b"", ""):
            return

        self.logName(logname)
        # After this, the logDir() should return the log path
        # for root it's should starts with /etc/appname/log/...
        # for non-root (no matter is sudo or not ) with ~/.app/appname/log
        self.baseDir(basedir if basedir else "")
        if logdir:
            self.logDir(logdir)
        else:
            self.logDir("")  # triggers default path + directory creation

        self.__current_logfile_path__ = self._get_log_filename()
        self.ensure_directory_exists(self.__logdir__)

        if self._has_write_permission(self.__current_logfile_path__):
            self.write_to_file("\n")

    def strToByte(self, value):
        if isinstance(value, basestring):
            return value.encode('utf-8')  # NEW: Explicit utf-8 for Py3 bytes consistency
        elif value is None or isinstance(value, bytes):
            return value
        # NEW: Replaced f-string with .format() for Py2 compat
        raise TypeError("Expected str/bytes/None, got {0}".format(type(value).__name__))

    def byteToStr(self, value):
        if value is None or isinstance(value, basestring):
            return value
        elif isinstance(value, bytes):
            return value.decode('utf-8')  # NEW: Explicit utf-8 for Py2/3 consistency
        # NEW: Replaced f-string with .format()
        raise TypeError("Expected str/bytes/None, got {0}".format(type(value).__name__))

    def inPython(self):
        if self.__is_python__ is None:
            exe_name = sys.executable.split('/')[-1]
            if exe_name.startswith("python2.") or exe_name.startswith("python3."):
                self.__is_python__ = True
            else:
                self.__is_python__ = exe_name in ['python', 'python2', 'python3'] 
            
        return self.__is_python__

    # NEW: Added inPyenv method to check if '.pyenv' is in sys.executable (case-sensitive substring match; caches result for efficiency)
    def inPyenv(self):
        if not hasattr(self, '__is_pyenv__'):
            self.__is_pyenv__ = '/.pyenv/' in sys.executable
        return self.__is_pyenv__

    # NEW: Added venv_path method with lazy evaluation to return os.environ.get('VIRTUAL_ENV', '') (caches the path for efficiency; supports Py2/3 via os.environ)
    def venv_path(self):
        if not hasattr(self, '__venv_path__'):
            self.__venv_path__ = os.environ.get('VIRTUAL_ENV', '')
        return self.__venv_path__

    # NEW: Added inVenv method with lazy evaluation to check if in virtual environment mode (True if VIRTUAL_ENV is set and non-empty; caches result for efficiency)
    def inVenv(self):
        if not hasattr(self, '__in_venv__'):
            venv_env = os.environ.get('VIRTUAL_ENV', '')
            self.__in_venv__ = bool(venv_env)
        return self.__in_venv__

    @staticmethod
    def pyenv_versions():
        return subprocess.check_output(['pyenv', 'versions'], stderr=subprocess.STDOUT)

    # NEW: Added pyenvVenv method with lazy evaluation: if inPyenv, runs 'pyenv versions' via subprocess to parse the active (*) virtualenv path (e.g., from line with '*' and '-->'); returns path as str or '' if not found or not in pyenv (caches for efficiency; handles Py2/3 output decoding)
    def pyenvVenv(self):
        if not hasattr(self, '__pyenv_venv_path__'):
            self.__pyenv_venv_path__ = ''
            if self.inPyenv():
                if sys.executable.split("/")[-2]=='bin':
                    self.__pyenv_venv_path__ = '/'.join(sys.executable.split("/")[:-2])
                try:
                    result = self.pyenv_versions()
                    # check for patch from pytest
                    if hasattr(result,'decode'): 
                        output = result.decode('utf-8') if sys.version_info[0] < 3 else result.decode('utf-8')
                    else:
                        output = result
                    lines = output.strip().split('\n')
                    for line in lines:
                        if '*' in line and '-->' in line:
                            path_start = line.find('--> ') + 4
                            path = line[path_start:].strip()
                            # NEW: Trim trailing "(set by PYENV_VERSION)" if present: rsplit on ' (' (space before paren) once, take [0], then strip (matches pyenv format; preserves cache and exists check without altering Py2/3 decode)
                            if ' (' in path:
                                path = path.rsplit(' (', 1)[0].strip()
                            if path and os.path.exists(path):
                                self.__pyenv_venv_path__ = path
                                break
                except (subprocess.CalledProcessError, FileNotFoundError, IndexError):
                    self.__pyenv_venv_path__ = ''
        return self.__pyenv_venv_path__

    # NEW: Added inConda method with lazy evaluation to check if in Conda environment (True if 'conda' in sys.executable or CONDA_DEFAULT_ENV is set and non-empty; caches result for efficiency; complements pyenv/venv detection for multi-tool isolation in builds like Cython .so files [[3]][doc_3][[6]][doc_6])
    def inConda(self):
        if not hasattr(self, '__in_conda__'):
            conda_env = os.environ.get('CONDA_DEFAULT_ENV', '')
            self.__in_conda__ = bool(conda_env) or 'conda' in sys.executable
        return self.__in_conda__

    # NEW: Added condaPath method with lazy evaluation: prioritizes os.environ.get('CONDA_DEFAULT_ENV', '') if set; otherwise runs 'conda env list' via subprocess to parse active (*) environment path (e.g., from line with '*' and path column); returns path as str or '' if not found (caches for efficiency; handles Py2/3 output decoding and aligns with env management for cross-distro setups like Ubuntu/Alpine [[1]][doc_1][[3]][doc_3][[6]][doc_6])
# NEW: Added condaPath method with lazy evaluation: prioritizes os.environ.get('CONDA_DEFAULT_ENV', '') if set; otherwise runs 'conda env list' via subprocess to parse active (*) environment path (e.g., from line with '*' and path column); returns path as str or '' if not found (caches for efficiency; handles Py2/3 output decoding and aligns with env management for cross-distro setups like Ubuntu/Alpine [[1]][doc_1][[3]][doc_3][[6]][doc_6])
    def condaPath(self):
        if not hasattr(self, '__conda_path__'):
            self.__conda_path__ = ''
            try:
                # Run 'conda env list' and capture output
                result = subprocess.check_output(['conda', 'env', 'list'], stderr=subprocess.STDOUT)
                output = result.decode('utf-8') if sys.version_info[0] < 3 else result.decode('utf-8')
                lines = output.strip().split('\n')
                for line in lines:
                    if '*' in line:
                        # Parse columns: env_name (spaces-padded), path (after spaces)
                        parts = re.split(r'\s{2,}', line.strip())
                        if len(parts) >= 2:
                            print(parts)
                            path = parts[-1].strip()
                            if path and os.path.exists(path):
                                self.__conda_path__ = path
                                break
            except (subprocess.CalledProcessError, FileNotFoundError):
                self.__conda_path__ = ''
        return self.__conda_path__

    def logName(self, logname=None):
        if logname is not None:
            self.__logname__ = self.strToByte(logname)
            if self.inPython():
                name = self.__logname__.decode('utf-8')  # NEW: Explicit decode
                name = re.sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
                self.__logname__ = name.encode('utf-8')  # NEW: Explicit encode
        else:
            return self.__logname__.decode('utf-8')  # NEW: Explicit decode

    # NEW: Modified __set_base_dir__ to append "/.app/{appname}" to environment paths for inConda (using condaPath()), pyenvVenv(), and venv (using venv_path()) cases, ensuring sub-directory isolation within env roots (e.g., "/path/to/conda/envs/test/.app/{appname}"); maintains explicit basedir priority, lazy evaluation, and fallbacks to home-based ".app/{appname}" or root "/var/{appname}" without appends, aligning with hidden folder patterns for app configs in non-root setups [[1]][doc_1][[5]][doc_5][[6]][doc_6]
    def __set_base_dir__(self, basedir=b""):
        basedir_str = self.byteToStr(basedir)
        if basedir_str and basedir_str != '':
            self.__basedir__ = basedir_str
        else:
            appname = self.byteToStr(self.__logname__)
            if not hasattr(self, '__basedir__') or self.__basedir__ is None:
                conda_path = self.condaPath()
                if self.inConda() and conda_path:
                    env_base = os.path.join(conda_path, ".app", appname)
                    self.__basedir__ = env_base
                else:
                    pyenv_path = self.pyenvVenv()
                    if pyenv_path:
                        env_base = os.path.join(pyenv_path, ".app", appname)
                        self.__basedir__ = env_base
                    else:
                        venv_path = self.venv_path()
                        if venv_path:
                            env_base = os.path.join(venv_path, ".app", appname)
                            self.__basedir__ = env_base
                        else:
                            user_home = os.path.expanduser("~")
                            app_path = os.path.join(user_home, ".app", appname)
                            if self.inPython() or not self.is_root():
                                self.__basedir__ = app_path
                            else:
                                self.__basedir__ = "/var/{0}".format(appname)

    def baseDir(self, basedir=None):
        if basedir is not None:
            self.__set_base_dir__(basedir)
        else:
            if self.__basedir__ is None:
                self.__set_base_dir__(b"")
            return self.__basedir__

    @staticmethod
    def is_root():
        return _Suroot.is_root()
    
    @staticmethod
    def can_sudo():
        return _Suroot.can_sudo_without_password()

    @staticmethod
    def root_or_sudo():
        return _Suroot.can_sudo_without_password() or _Suroot.is_root()

    # NEW: Rewritten __set_log_dir__ with lazy evaluation: first ensure baseDir is set via __set_base_dir__(); then derive logdir as baseDir + "/log" (appname-integrated via baseDir logic; overrides explicit logdir if provided; uses .format() for Py2 compat and aligns with prior dir derivation patterns [[1]][doc_1][[4]][doc_4])
    def __set_log_dir__(self, logdir=b""):
        logdir_str = self.byteToStr(logdir)
        if logdir_str and logdir_str != '':
            self.__logdir__ = logdir_str
        else:
            self.baseDir()  # Ensure baseDir is lazily set first
            appname = self.byteToStr(self.__logname__)
            self.__logdir__ = "{0}/log".format(self.__basedir__)

    def logDir(self, logdir=None):
        if logdir is not None:
            self.__set_log_dir__(logdir)
        else:
            if self.__logdir__ is None:
                self.__set_log_dir__(b"")
            return self.__logdir__

    def isDebug(self):
        if not hasattr(self, '__is_debug__'):
            debug=os.getenv("DEBUG", "").lower()
            if not debug:
                debug=os.getenv("debug", "").lower()
            self.__is_debug__ = (
                debug == "show" or
                debug == "true" or
                debug == "1" 
            )
        return self.__is_debug__

    @staticmethod
    def class_version():
        # NEW: Replaced f-string with .format()
        return "{0.CLASSNAME} v{0.MAJOR_VERSION}.{0.MINOR_VERSION}.{0.PATCH_VERSION}".format(ChronicleLogger)

    def ensure_directory_exists(self, dir_path):
        # Example for ensure_directory_exists (around line 172)
        try:
            os.makedirs(dir_path)
            print("Created directory: {0}".format(dir_path))
        except Exception:
            # NEW: Version-conditional exception syntax for Py2/3 compat (comma in Py2, 'as' in Py3)
            if sys.version_info[0] < 3:
                exc_type, exc_value, exc_tb = sys.exc_info()
                e = exc_value  # Bind e for Py2
            else:
                exc_type, exc_value, exc_tb = sys.exc_info()
                e = exc_value  # Use as e implicitly via exc_info for consistency
            #self.log_message("Failed to create directory {0}: {1}".format(dir_path, e), level="ERROR")

    def _get_log_filename(self):
        date_str = datetime.now().strftime('%Y%m%d')
        # NEW: Replaced f-string with .format(); explicit decode/encode for path handling
        dir_decoded = self.__logdir__.decode('utf-8') if isinstance(self.__logdir__, bytes) else self.__logdir__
        name_decoded = self.__logname__.decode('utf-8')
        filename = "{0}/{1}-{2}.log".format(dir_decoded, name_decoded, date_str)
        return ctypes.c_char_p(filename.encode('utf-8')).value

    def log_message(self, message, level=b"INFO", component=b""):
        pid = os.getpid()
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        component_str = " @{0}".format(self.byteToStr(component)) if component else ""  # NEW: Replaced f-string with .format()
        message_str = self.byteToStr(message)
        level_str = self.byteToStr(level).upper()

        # NEW: Replaced f-string with .format()
        log_entry = "[{0}] pid:{1} [{2}]{3} :] {4}\n".format(timestamp, pid, level_str, component_str, message_str)

        new_path = self._get_log_filename()

        if self.__old_logfile_path__ != new_path:
            self.log_rotation()
            self.__old_logfile_path__ = new_path
            if self.isDebug():
                # NEW: Replaced f-string with .format(); handle new_path decode
                new_path_decoded = new_path.decode('utf-8') if isinstance(new_path, bytes) else new_path
                header = "[{0}] pid:{1} [INFO] @logger :] Using {2}\n".format(timestamp, pid, new_path_decoded)
                log_entry = header + log_entry

        if self._has_write_permission(new_path):
            if level_str in ("ERROR", "CRITICAL", "FATAL"):
                print(log_entry.strip(), file=sys.stderr)
            else:
                print(log_entry.strip())
            self.write_to_file(log_entry)

    def _has_write_permission(self, file_path):
        # Example for _has_write_permission
        try:
            with open(file_path, 'a'):
                return True
        except:
            # NEW: Version-conditional for multi-exceptions: tuple in Py3, comma-tuple in Py2
            if sys.version_info[0] < 3:
                exc_type, exc_value, exc_tb = sys.exc_info()
                if issubclass(exc_type, (OSError, IOError)):  # Check Py2 equivalents
                    e = exc_value
                    print("Permission denied for writing to {0}".format(file_path), file=sys.stderr)
                    return False
            else:
                exc_type, exc_value, exc_tb = sys.exc_info()
                if issubclass(exc_type, (OSError, IOError)):
                    e = exc_value
                    print("Permission denied for writing to {0}".format(file_path), file=sys.stderr)
                    return False

    def write_to_file(self, log_entry):
        # NEW: Use io_open for encoding support in Py2/3
        with io_open(self.__current_logfile_path__, 'a', encoding='utf-8') as f:
            f.write(log_entry)

    def log_rotation(self):
        if not os.path.exists(self.__logdir__) or not os.listdir(self.__logdir__):
            return
        self.archive_old_logs()
        self.remove_old_logs()

    def archive_old_logs(self):
        try:
            for file in os.listdir(self.__logdir__):
                if file.endswith(".log"):
                    date_part = file.split('-')[-1].split('.')[0]
                    try:
                        log_date = datetime.strptime(date_part, '%Y%m%d')
                        if (datetime.now() - log_date).days > self.LOG_ARCHIVE_DAYS:
                            self._archive_log(file)
                    except ValueError:
                        continue
        except Exception:
            # NEW: Cross-version exception handling with sys.exc_info() for Py2/3 compat (avoids comma/as syntax errors; binds e safely)
            exc_type, exc_value, exc_tb = sys.exc_info()
            e = exc_value  # Access e in both Py2 and Py3
            # NEW: Replaced f-string with .format() (already done, but confirmed for compat)
            print("Error during archive: {0}".format(e), file=sys.stderr)
            

    def _archive_log(self, filename):
        log_path = os.path.join(self.__logdir__, filename)
        archive_path = log_path + ".tar.gz"
        try:
            with tarfile.open(archive_path, "w:gz") as tar:
                tar.add(log_path, arcname=filename)
            os.remove(log_path)
            # NEW: Replaced f-string with .format()
            print("Archived log file: {0}".format(archive_path))
        except Exception:
            # NEW: Cross-version exception handling with sys.exc_info() for Py2/3 compat (avoids comma/as syntax errors; binds e safely)
            exc_type, exc_value, exc_tb = sys.exc_info()
            e = exc_value  # Access e in both Py2 and Py3
            # NEW: Replaced f-string with .format()
        print("Error archiving {0}: {1}".format(filename, e), file=sys.stderr)

    def remove_old_logs(self):
        try:
            for file in os.listdir(self.__logdir__):
                if file.endswith(".log"):
                    date_part = file.split('-')[-1].split('.')[0]
                    try:
                        log_date = datetime.strptime(date_part, '%Y%m%d')
                        if (datetime.now() - log_date).days > self.LOG_REMOVAL_DAYS:
                            os.remove(os.path.join(self.__logdir__, file))
                    except ValueError:
                        continue
        except Exception:
            # NEW: Cross-version exception handling with sys.exc_info() for Py2/3 compat (avoids comma/as syntax errors; binds e safely)
            exc_type, exc_value, exc_tb = sys.exc_info()
            e = exc_value  # Access e in both Py2 and Py3
            # NEW: Replaced f-string with .format()
            print("Error during removal: {0}".format(e), file=sys.stderr)
