import logging
import os
import re
import glob
import contextvars
import time
from logging.handlers import RotatingFileHandler

from pypos.core.utils.config_utils import read_app_settings
from pypos.core.utils.path_utils import get_app_data_dir

_LOG_CONTEXT = contextvars.ContextVar("pypos_log_context", default={})


def _to_int(value, default):
    try:
        return int(value)
    except (TypeError, ValueError):
        return int(default)


def _to_bool(value, default=True):
    if isinstance(value, bool):
        return value
    if value is None:
        return bool(default)
    return str(value).strip().lower() in ("1", "true", "yes", "on")


def _normalize_context_fields(value):
    if isinstance(value, (list, tuple, set)):
        items = list(value)
    else:
        raw = str(value or "").strip()
        items = raw.split(",") if raw else []
    fields = []
    for item in items:
        name = str(item or "").strip()
        if name and name not in fields:
            fields.append(name)
    if not fields:
        fields = ["user_id", "machine_id", "cabang_id"]
    return tuple(fields)


def _normalize_sensitive_keys(value):
    if isinstance(value, (list, tuple, set)):
        items = list(value)
    else:
        raw = str(value or "").strip()
        items = raw.split(",") if raw else []
    keys = []
    for item in items:
        key = str(item or "").strip().lower()
        if key and key not in keys:
            keys.append(key)
    if not keys:
        keys = ["password", "passwd", "token", "authorization", "api_key", "pin", "kode_voucher"]
    return tuple(keys)


def set_log_context(**kwargs):
    current = dict(_LOG_CONTEXT.get() or {})
    for key, value in (kwargs or {}).items():
        if value is None:
            current.pop(str(key), None)
        else:
            current[str(key)] = str(value)
    _LOG_CONTEXT.set(current)
    return dict(current)


def get_log_context():
    return dict(_LOG_CONTEXT.get() or {})


def clear_log_context(*keys):
    if not keys:
        _LOG_CONTEXT.set({})
        return {}
    current = dict(_LOG_CONTEXT.get() or {})
    for key in keys:
        current.pop(str(key), None)
    _LOG_CONTEXT.set(current)
    return dict(current)


class LogContextFilter(logging.Filter):
    def __init__(self, fields, enabled=True):
        super().__init__()
        self.fields = tuple(fields or ())
        self.enabled = bool(enabled)

    def filter(self, record):
        context = get_log_context() if self.enabled else {}
        for field in self.fields:
            value = context.get(field)
            setattr(record, field, str(value) if value not in (None, "") else "-")
        return True


class SensitiveDataFilter(logging.Filter):
    def __init__(self, sensitive_keys):
        super().__init__()
        self.sensitive_keys = {str(k).strip().lower() for k in (sensitive_keys or []) if str(k).strip()}
        escaped = "|".join(re.escape(k) for k in sorted(self.sensitive_keys, key=len, reverse=True))
        if not escaped:
            escaped = "password"
        self._json_pattern = re.compile(
            rf'(?P<key>"(?:{escaped})")(?P<sep>\s*:\s*)(?P<value>"[^"]*"|\'[^\']*\'|[^,\}}\s]+)',
            flags=re.IGNORECASE,
        )
        self._kv_pattern = re.compile(
            rf"(?P<key>\b(?:{escaped})\b)(?P<sep>\s*[:=]\s*)(?P<value>\"[^\"]*\"|'[^']*'|[^,\s;]+)",
            flags=re.IGNORECASE,
        )

    def _is_sensitive_key(self, key):
        return str(key or "").strip().lower() in self.sensitive_keys

    def _sanitize_text(self, value):
        text = str(value)
        text = self._json_pattern.sub(lambda m: f'{m.group("key")}{m.group("sep")}"***"', text)
        text = self._kv_pattern.sub(lambda m: f'{m.group("key")}{m.group("sep")}***', text)
        return text

    def _sanitize_any(self, value, key=None):
        if key is not None and self._is_sensitive_key(key):
            return "***"
        if isinstance(value, dict):
            return {k: self._sanitize_any(v, key=k) for k, v in value.items()}
        if isinstance(value, list):
            return [self._sanitize_any(v) for v in value]
        if isinstance(value, tuple):
            return tuple(self._sanitize_any(v) for v in value)
        if isinstance(value, set):
            return {self._sanitize_any(v) for v in value}
        if isinstance(value, str):
            return self._sanitize_text(value)
        return value

    def filter(self, record):
        try:
            record.msg = self._sanitize_any(record.msg)
            if isinstance(record.args, tuple):
                record.args = tuple(self._sanitize_any(arg) for arg in record.args)
            elif isinstance(record.args, dict):
                record.args = {k: self._sanitize_any(v, key=k) for k, v in record.args.items()}
            elif record.args:
                record.args = self._sanitize_any(record.args)
        except (TypeError, ValueError, RuntimeError):
            return True
        return True


def _attach_filters(handler, filters):
    for flt in filters:
        if flt is not None:
            handler.addFilter(flt)


def _prune_old_logs(log_dir, log_file_name, retention_days):
    if retention_days <= 0:
        return 0
    pattern = os.path.join(log_dir, f"{log_file_name}*")
    now_ts = time.time()
    cutoff = retention_days * 86400
    removed = 0
    for path in glob.glob(pattern):
        if not os.path.isfile(path):
            continue
        try:
            age = now_ts - os.path.getmtime(path)
            if age > cutoff:
                os.remove(path)
                removed += 1
        except OSError:
            continue
    return removed


def _resolve_log_storage_config(settings=None):
    cfg = settings if isinstance(settings, dict) else (read_app_settings() or {})
    base_dir = get_app_data_dir()
    log_dir_cfg = str(cfg.get("log_dir") or "logs").strip()
    log_dir = log_dir_cfg if os.path.isabs(log_dir_cfg) else os.path.join(base_dir, log_dir_cfg)
    log_file_name = str(cfg.get("log_file_name") or "pypos.log").strip()
    retention_days = max(0, _to_int(cfg.get("log_file_retention_days"), 30))
    return {
        "log_dir": log_dir,
        "log_file_name": log_file_name,
        "retention_days": retention_days,
    }


def prune_logs_now(settings=None):
    storage = _resolve_log_storage_config(settings=settings)
    os.makedirs(storage["log_dir"], exist_ok=True)
    return _prune_old_logs(
        storage["log_dir"],
        storage["log_file_name"],
        storage["retention_days"],
    )


def get_log_prune_interval_minutes(settings=None):
    cfg = settings if isinstance(settings, dict) else (read_app_settings() or {})
    interval_minutes = _to_int(cfg.get("log_prune_interval_minutes"), 0)
    return max(0, interval_minutes)


def configure_logging(force=False):
    settings = read_app_settings() or {}

    level_name = str(settings.get("log_level") or "INFO").upper()
    level_num = getattr(logging, level_name, logging.INFO)

    log_format = str(
        settings.get("log_format")
        or "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
    )
    log_datefmt = str(settings.get("log_date_format") or "%Y-%m-%d %H:%M:%S")
    context_fields = _normalize_context_fields(settings.get("log_context_fields"))
    sensitive_keys = _normalize_sensitive_keys(settings.get("log_redact_keys"))

    root_logger = logging.getLogger()
    already_configured = getattr(root_logger, "_pypos_logging_configured", False)
    if already_configured and not force:
        return

    if already_configured and force:
        for handler in list(root_logger.handlers):
            try:
                handler.close()
            except OSError:
                pass
            root_logger.removeHandler(handler)

    root_logger.setLevel(level_num)
    formatter = logging.Formatter(log_format, datefmt=log_datefmt)
    context_filter = LogContextFilter(
        context_fields,
        enabled=_to_bool(settings.get("log_context_enabled", 1), True),
    )
    sensitive_filter = SensitiveDataFilter(sensitive_keys) if _to_bool(settings.get("log_redact_enabled", 1), True) else None
    shared_filters = [context_filter, sensitive_filter]

    if _to_bool(settings.get("log_console", 1), True):
        console_handler = logging.StreamHandler()
        console_level_name = str(settings.get("log_console_level") or level_name).upper()
        console_handler.setLevel(getattr(logging, console_level_name, level_num))
        console_handler.setFormatter(formatter)
        _attach_filters(console_handler, shared_filters)
        root_logger.addHandler(console_handler)

    if _to_bool(settings.get("log_to_file", 1), True):
        storage = _resolve_log_storage_config(settings=settings)
        log_dir = storage["log_dir"]
        log_file_name = storage["log_file_name"]
        retention_days = storage["retention_days"]
        os.makedirs(log_dir, exist_ok=True)
        log_path = os.path.join(log_dir, log_file_name)
        if _to_bool(settings.get("log_file_prune_on_start", 1), True):
            _prune_old_logs(log_dir, log_file_name, retention_days)

        max_bytes = max(1024, _to_int(settings.get("log_file_max_bytes"), 5 * 1024 * 1024))
        backup_count = max(1, _to_int(settings.get("log_file_backup_count"), 5))
        file_level_name = str(settings.get("log_file_level") or level_name).upper()
        file_level = getattr(logging, file_level_name, level_num)

        file_handler = RotatingFileHandler(
            log_path,
            maxBytes=max_bytes,
            backupCount=backup_count,
            encoding="utf-8",
        )
        file_handler.setLevel(file_level)
        file_handler.setFormatter(formatter)
        _attach_filters(file_handler, shared_filters)
        root_logger.addHandler(file_handler)

    root_logger._pypos_logging_configured = True
