from __future__ import annotations

import os
import re
import hashlib
import time
import uuid
import zipfile
from pathlib import Path
from typing import Sequence

from common.runtime_paths import cloud_data_dir, cloud_log_dir


LOG_RETENTION_DAYS = int(os.environ.get("WAWA_LOG_RETENTION_DAYS", "3") or "3")
LOG_MAX_FILES_PER_FOLDER = int(os.environ.get("WAWA_LOG_MAX_FILES_PER_FOLDER", "200") or "200")
LOG_EXPORT_MAX_FILES = int(os.environ.get("WAWA_LOG_EXPORT_MAX_FILES", "30") or "30")
LOG_EXPORT_RECENT_RUNS = 2
LOG_EXPORT_MAX_PACKAGES = int(os.environ.get("WAWA_LOG_EXPORT_MAX_PACKAGES", "30") or "30")
LOG_EXPORT_PREFIX = "云管理日志"
RUN_INSTANCE_ID = f"pid{os.getpid()}-{uuid.uuid4().hex[:8]}"


def safe_log_name(name: str) -> str:
    """把功能名称转换成适合文件夹和文件名的安全文本。"""
    value = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff_-]+", "-", str(name or "").strip())
    return value.strip("-") or "日志"


def credential_scope_label(ak: str = "", sk: str = "") -> str:
    """用当前 AK/SK 生成不暴露密钥明文的账号日志分组名。"""
    ak_text = str(ak or "").strip()
    sk_text = str(sk or "").strip()
    if not ak_text and not sk_text:
        return "账号-未识别"
    digest = hashlib.sha256(f"{ak_text}\n{sk_text}".encode("utf-8", errors="ignore")).hexdigest()[:10]
    ak_tail = safe_log_name(ak_text[-6:] if ak_text else "no-ak")
    return f"账号-{ak_tail}-{digest}"


_BUILD_COMMAND_PATTERNS = (
    re.compile(r"(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*bash\s+<\(\s*curl\b[^\r\n]*\)", re.IGNORECASE),
    re.compile(r"\bcurl\s+(?:-[A-Za-z0-9]+\s+)*[^\s\r\n]*(?:wawaapi\.com/static/scripts|/down/)[^\s\r\n]*", re.IGNORECASE),
    re.compile(r"https?://(?:www\.)?wawaapi\.com/static/scripts/[^\s'\"）)]+", re.IGNORECASE),
)


def sanitize_log_text(text: object) -> str:
    value = "" if text is None else str(text)
    if not value:
        return ""
    lines = value.splitlines(keepends=True)
    if not lines:
        return ""
    return "".join(
        line
        for line in lines
        if not any(pattern.search(line) for pattern in _BUILD_COMMAND_PATTERNS)
    )


def _timestamp_with_instance() -> str:
    millis = int((time.time() % 1) * 1000)
    return f"{time.strftime('%Y%m%d-%H%M%S')}-{millis:03d}-{RUN_INSTANCE_ID}"


def _cleanup_empty_dirs(root: Path) -> None:
    for path in sorted(root.rglob("*"), key=lambda item: len(item.parts), reverse=True):
        if path.is_dir():
            try:
                path.rmdir()
            except OSError:
                pass


def cleanup_old_logs(root: Path, *, days: int = LOG_RETENTION_DAYS, max_files: int = LOG_MAX_FILES_PER_FOLDER) -> None:
    """清理旧日志：默认只保留 3 天，同时限制单个目录日志数量。"""
    if not root.exists():
        return
    now = time.time()
    expire_before = now - max(1, days) * 24 * 60 * 60
    for path in root.rglob("*"):
        if not path.is_file() or path.suffix.lower() not in {".log", ".txt"}:
            continue
        try:
            if path.stat().st_mtime < expire_before:
                path.unlink()
        except OSError:
            pass

    for folder in [root, *[item for item in root.rglob("*") if item.is_dir()]]:
        files = sorted(
            [item for item in folder.iterdir() if item.is_file() and item.suffix.lower() in {".log", ".txt"}],
            key=lambda item: item.stat().st_mtime,
            reverse=True,
        )
        for old_file in files[max_files:]:
            try:
                old_file.unlink()
            except OSError:
                pass
    _cleanup_empty_dirs(root)


def cleanup_old_exports(root: Path, *, days: int = LOG_RETENTION_DAYS, max_files: int = LOG_EXPORT_MAX_PACKAGES) -> None:
    """只清理本软件导出的日志压缩包，不碰用户桌面的其它文件。"""
    if not root.exists():
        return
    now = time.time()
    expire_before = now - max(1, days) * 24 * 60 * 60
    files = sorted(root.glob(f"{LOG_EXPORT_PREFIX}-*.zip"), key=lambda item: item.stat().st_mtime, reverse=True)
    for path in files:
        try:
            if path.stat().st_mtime < expire_before:
                path.unlink()
        except OSError:
            pass
    for old_file in files[max_files:]:
        try:
            old_file.unlink()
        except OSError:
            pass


def cleanup_old_runtime(root: Path, *, days: int = LOG_RETENTION_DAYS, keep: Path | None = None) -> None:
    """清理旧任务运行状态，避免多实例隔离目录长期堆积。"""
    if not root.exists():
        return
    now = time.time()
    expire_before = now - max(1, days) * 24 * 60 * 60
    keep_path: Path | None = None
    if keep is not None:
        try:
            keep_path = keep.resolve()
        except OSError:
            keep_path = keep

    def should_keep(path: Path) -> bool:
        if keep_path is None:
            return False
        try:
            resolved = path.resolve()
        except OSError:
            resolved = path
        return resolved == keep_path or keep_path in resolved.parents or resolved in keep_path.parents

    for path in sorted(root.rglob("*"), key=lambda item: len(item.parts), reverse=True):
        if should_keep(path):
            continue
        try:
            if path.is_file():
                if path.stat().st_mtime < expire_before:
                    path.unlink()
            elif path.is_dir() and path.stat().st_mtime < expire_before:
                path.rmdir()
        except OSError:
            pass


def task_log_dir(cloud_name: str, section: str, action: str, *, account_key: str = "") -> Path:
    """返回按云、页面、功能拆分后的日志目录。"""
    root = cloud_log_dir(cloud_name)
    cleanup_old_logs(root)
    parts = []
    if account_key:
        parts.append(safe_log_name(account_key))
    parts.extend([safe_log_name(section), safe_log_name(action)])
    path = root
    for part in parts:
        path /= part
    path.mkdir(parents=True, exist_ok=True)
    return path


def create_task_log_path(cloud_name: str, section: str, action: str, *, prefix: str = "run", account_key: str = "") -> Path:
    path = task_log_dir(cloud_name, section, action, account_key=account_key) / f"{safe_log_name(prefix)}-{_timestamp_with_instance()}.log"
    return path


def task_runtime_dir(cloud_name: str, section: str, action: str, detail: str = "", *, account_key: str = "") -> Path:
    """返回按云、页面、功能拆分后的运行状态目录。"""
    runtime_root = cloud_data_dir(cloud_name) / "runtime"
    parts = ["runtime", safe_log_name(section), safe_log_name(action)]
    if account_key:
        parts.append(safe_log_name(account_key))
    if detail:
        parts.append(safe_log_name(detail))
    path = cloud_data_dir(cloud_name)
    for part in parts:
        path /= part
    path.mkdir(parents=True, exist_ok=True)
    cleanup_old_runtime(runtime_root, keep=path)
    cleanup_old_logs(path)
    return path


def desktop_export_root() -> Path:
    root = Path.home() / "Desktop"
    root.mkdir(parents=True, exist_ok=True)
    cleanup_old_exports(root)
    return root


def _log_section_dir(cloud_name: str, section: str, *, account_key: str = "") -> Path:
    root = cloud_log_dir(cloud_name)
    parts = []
    if account_key:
        parts.append(safe_log_name(account_key))
    parts.append(safe_log_name(section))
    path = root
    for part in parts:
        path /= part
    return path


def _recent_logs_from_source(source_dir: Path) -> list[Path]:
    if not source_dir.exists():
        return []
    return sorted(
        [item for item in source_dir.rglob("*") if item.is_file() and item.suffix.lower() in {".log", ".txt"}],
        key=lambda item: item.stat().st_mtime,
        reverse=True,
    )


def _read_log_text_for_export(path: Path) -> str:
    for encoding in ("utf-8-sig", "utf-8", "gbk"):
        try:
            return path.read_text(encoding=encoding)
        except UnicodeDecodeError:
            continue
        except OSError:
            return ""
    try:
        return path.read_bytes().decode("utf-8", errors="replace")
    except OSError:
        return ""


def export_log_to_desktop(
    cloud_name: str,
    section: str,
    action: str,
    text: str,
    *,
    account_key: str = "",
    current_logs: Sequence[tuple[str, str, str]] = (),
    recent_sections: Sequence[str] = (),
    recent_sources: Sequence[tuple[str, str]] = (),
    metadata_lines: Sequence[str] = (),
) -> Path:
    """点击导出时生成桌面 zip，压缩包内部按云、页面、功能分目录。"""
    cloud = safe_log_name(cloud_name)
    page = safe_log_name(section)
    task = safe_log_name(action)
    account = safe_log_name(account_key) if account_key else ""
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    export_root = desktop_export_root()
    account_part = f"-{account}" if account else ""
    path = export_root / f"{LOG_EXPORT_PREFIX}-{cloud}{account_part}-{page}-{task}-{timestamp}.zip"
    source_roots: list[tuple[str, Path]] = []
    if recent_sections:
        for item_section in recent_sections:
            section_name = str(item_section or "").strip()
            if section_name:
                source_roots.append((safe_log_name(section_name), _log_section_dir(cloud_name, section_name, account_key=account_key)))
    else:
        source_roots.append((f"{page}/{task}", task_log_dir(cloud_name, section, action, account_key=account_key)))
    for item_section, item_action in recent_sources:
        section_name = str(item_section or "").strip()
        action_name = str(item_action or "").strip()
        if section_name and action_name:
            source_roots.append(
                (
                    f"{safe_log_name(section_name)}/{safe_log_name(action_name)}",
                    task_log_dir(cloud_name, section_name, action_name, account_key=account_key),
                )
            )
    seen_files: set[Path] = set()
    recent_logs: list[tuple[str, Path, Path]] = []
    for source_label, source_root in source_roots:
        for item in _recent_logs_from_source(source_root):
            try:
                file_key = item.resolve()
            except OSError:
                file_key = item
            if file_key in seen_files:
                continue
            seen_files.add(file_key)
            recent_logs.append((source_label, source_root, item))
    recent_logs.sort(key=lambda item: item[2].stat().st_mtime, reverse=True)
    recent_logs = recent_logs[: max(1, min(LOG_EXPORT_MAX_FILES, LOG_EXPORT_RECENT_RUNS))]
    current_log_entries = [
        (str(item_section or "").strip(), str(item_action or "").strip(), sanitize_log_text(item_text).strip())
        for item_section, item_action, item_text in current_logs
        if str(item_text or "").strip()
    ]
    if not current_log_entries and str(text or "").strip():
        current_log_entries = [(section, action, sanitize_log_text(text).strip())]
    combined_text_parts = []
    for item_section, item_action, item_text in current_log_entries:
        combined_text_parts.extend(
            [
                f"[{item_section or '未区分页面'} - {item_action or '界面日志'}]",
                item_text,
                "",
            ]
        )
    combined_text = "\n".join(combined_text_parts).rstrip()
    export_metadata = []
    for item in metadata_lines:
        safe_item = sanitize_log_text(item).strip()
        if safe_item:
            export_metadata.append(safe_item)
    note = "\n".join(
        [
            "云管理日志导出包",
            f"云: {cloud_name}",
            f"账号分组: {account or '未区分'}",
            f"页面: {section}",
            f"功能: {action}",
            f"导出时间: {time.strftime('%Y-%m-%d %H:%M:%S')}",
            f"当前界面日志数量: {len(current_log_entries)}",
            f"最近运行日志数量: {len(recent_logs)}",
            *export_metadata,
            "",
        ]
    )
    with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        archive.writestr("导出说明.txt", note)
        base = f"{cloud}/{account}/{page}/{task}" if account else f"{cloud}/{page}/{task}"
        archive.writestr(f"{base}/当前界面日志.txt", combined_text + ("\n" if combined_text else ""))
        for index, (item_section, item_action, item_text) in enumerate(current_log_entries, start=1):
            item_name = safe_log_name(f"{index}-{item_section}-{item_action}")
            archive.writestr(f"{base}/当前界面日志/{item_name}.txt", item_text.rstrip() + "\n")
        for source_label, source_root, item in recent_logs:
            try:
                rel_name = item.relative_to(source_root).as_posix()
            except ValueError:
                rel_name = item.name
            archive.writestr(
                f"{base}/最近运行日志/{source_label}/{rel_name}",
                sanitize_log_text(_read_log_text_for_export(item)),
            )
    cleanup_old_exports(export_root)
    return path
