import time
import os
import getpass
import glob
import logging
import portalocker
import atexit
import shutil
from pathlib import Path
from abc import ABC, abstractmethod

class MultiProcessSafeRotatingHandlerBase(logging.Handler, ABC):
    def __init__(self, filename, backupCount=3):
        super().__init__()
        self.filename = filename
        self.backupCount = backupCount

        # --- 修改策略：所有辅助文件（锁、临时文件）都放入一个隐藏子目录 ---
        
        # 1. 获取日志文件所在的绝对目录
        # 例如 filename = "logs/app.log" -> log_parent_dir = "/.../project/logs"
        self.log_parent_dir = os.path.dirname(os.path.abspath(filename))
        
        # 2. 定义“收纳”目录，使用 "." 开头通常表示隐藏或配置目录
        # 例如 "/.../project/logs/.logging_locks"
        self.aux_dir = os.path.join(self.log_parent_dir, ".logging_locks")
        
        # 3. 确保这个隐藏目录存在 (exist_ok=True 保证多进程并发创建时不报错)
        os.makedirs(self.aux_dir, exist_ok=True)

        # 4. 定义锁文件路径 (放入隐藏目录)
        # 例如 "/.../project/logs/.logging_locks/app.log.lock"
        self.lockfile = os.path.join(self.aux_dir, os.path.basename(filename) + ".lock")

        # 5. 定义临时文件路径 (也放入隐藏目录，进一步净化主目录)
        self.pid = os.getpid()
        self.user = getpass.getuser()
        # 例如 "/.../project/logs/.logging_locks/app.log.tmp.user.12345"
        self.tmp_file = os.path.join(self.aux_dir, f"{os.path.basename(filename)}.tmp.{self.user}.{self.pid}")
        
        atexit.register(self.close)

    def emit(self, record):
        try:
            msg = self.format(record) + '\n'
            
            # 策略：尝试在 0.2 秒内获取锁
            lock_fd = self._acquire_lock(timeout=0.2)
            
            if lock_fd:
                try:
                    self._flush_my_temp_to_main()
                    self._append_to_main(msg)
                    if self._should_rollover(record):
                        self.doRollover()
                finally:
                    os.close(lock_fd)
            else:
                self._write_to_tmp(msg)
                
        except Exception:
            self.handleError(record)

    def close(self):
        """Handler 关闭时的清理逻辑"""
        # 尝试合并最后的数据
        lock_fd = self._acquire_lock(timeout=2.0)
        if lock_fd:
            try:
                self._flush_my_temp_to_main()
            finally:
                os.close(lock_fd)
        
        # 尝试清理自己的锁文件 (尽力而为)
        self._try_clean_lockfile()
        super().close()

    def _try_clean_lockfile(self):
        """尝试删除锁文件，如果失败说明还有其他进程在用，忽略即可"""
        if os.path.exists(self.lockfile):
            try:
                os.remove(self.lockfile)
            except OSError:
                pass
        
        # 可选：如果目录空了，也可以尝试删除目录 (保持目录更干净)
        # try:
        #     os.rmdir(self.aux_dir)
        # except OSError:
        #     pass

    def _acquire_lock(self, timeout=0.5):
        """获取文件锁"""
        start_time = time.time()
        fd = None
        
        # 双重保险：虽然 init 里创建了目录，但防止运行时目录被意外删除
        if not os.path.exists(self.aux_dir):
            try:
                os.makedirs(self.aux_dir, exist_ok=True)
            except OSError:
                pass

        try:
            fd = os.open(self.lockfile, os.O_CREAT | os.O_WRONLY)
        except OSError:
            return None

        while True:
            try:
                portalocker.lock(fd, portalocker.LOCK_EX | portalocker.LOCK_NB)
                return fd 
            except (portalocker.LockException, BlockingIOError):
                pass

            if time.time() - start_time > timeout:
                os.close(fd)
                return None
            
            time.sleep(0.01)

    def _write_to_tmp(self, msg):
        try:
            # 这里的 self.tmp_file 已经在 .logging_locks 目录里了
            with open(self.tmp_file, 'a', encoding='utf-8') as f:
                f.write(msg)
        except Exception:
            pass

    def _flush_my_temp_to_main(self):
        if not os.path.exists(self.tmp_file):
            return

        try:
            with open(self.tmp_file, 'r', encoding='utf-8') as f:
                content = f.read()
            
            if content:
                self._append_to_main(content)
            
            # 也是尽力删除临时文件
            with open(self.tmp_file, 'w', encoding='utf-8') as f:
                pass
            try:
                os.remove(self.tmp_file)
            except OSError:
                pass
                
        except Exception:
            pass

    def _append_to_main(self, data):
        with open(self.filename, 'a', encoding='utf-8') as f:
            f.write(data)
            f.flush()

    def doRollover(self):
        self._merge_all_temp_files() 
        self._do_rollover_impl()

    def _merge_all_temp_files(self):
        """
        合并 .logging_locks 目录下所有相关的临时文件
        """
        # 修改匹配模式，指向 aux_dir
        base_name = os.path.basename(self.filename)
        # 匹配模式 e.g.: /path/to/.logging_locks/app.log.tmp.*
        tmp_pattern = os.path.join(self.aux_dir, f"{base_name}.tmp.*")
        
        for tmp_path in glob.glob(tmp_pattern):
            if tmp_path == self.tmp_file: continue 

            try:
                merging_path = tmp_path + ".merging"
                os.rename(tmp_path, merging_path)
                
                with open(merging_path, 'r', encoding='utf-8') as f:
                    data = f.read()
                
                if data:
                    self._append_to_main(data)
                
                os.remove(merging_path)
            except OSError:
                continue

    @abstractmethod
    def _should_rollover(self, record) -> bool: pass
    @abstractmethod
    def _do_rollover_impl(self): pass
    def _get_file_size(self, filepath):
        try: return os.path.getsize(filepath)
        except OSError: return 0

class MultiProcessSafeSizeRotatingHandler(MultiProcessSafeRotatingHandlerBase):
    def __init__(self, filename, maxBytes=5 * 1024 * 1024, backupCount=3):
        if isinstance(maxBytes, str):
            if maxBytes.upper().endswith('M'): maxBytes = int(float(maxBytes[:-1])*1024*1024)
            elif maxBytes.upper().endswith('K'): maxBytes = int(float(maxBytes[:-1])*1024)
            else: maxBytes = int(maxBytes)
            
        super().__init__(filename, backupCount)
        self.maxBytes = maxBytes

    def _should_rollover(self, record) -> bool:
        return self._get_file_size(self.filename) >= self.maxBytes

    def _do_rollover_impl(self):
        log_path = Path(self.filename)
        for i in range(self.backupCount - 1, 0, -1):
            sfn = log_path.with_suffix(f'.log.{i}')
            dfn = log_path.with_suffix(f'.log.{i+1}')
            if sfn.exists():
                try:
                    if dfn.exists(): dfn.unlink()
                    sfn.rename(dfn)
                except OSError: pass

        if log_path.exists():
            dfn = log_path.with_suffix('.log.1')
            try:
                if dfn.exists(): dfn.unlink()
                log_path.rename(dfn)
            except OSError: pass
        
        with open(self.filename, 'w') as f: pass