import json
import hashlib
import time
from pathlib import Path
import shutil
import tempfile
from datetime import datetime
from .constants import DATA_DIR, DEFAULT_DATA_DIR

BACKUP_DIR = DATA_DIR / "backups"

# Runtime save cache: reduces repeated writes during autosave / page refresh.
# Keyed by absolute data path, it lets us skip unchanged JSON and avoid making
# backup copies every second while typing.
_JSON_HASH_CACHE = {}
_LAST_BACKUP_AT = {}
_LINK_METADATA_CACHE = {"loaded_at": 0.0, "personal_puid": "", "enterprise_puid": "", "workspace_puid": ""}
_SETTINGS_MODE_CACHE = {"mtime": None, "mode": "enterprise"}


def _json_hash(text: str) -> str:
    return hashlib.sha1(text.encode("utf-8")).hexdigest()


def ensure_data_dir():
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    # Copy bundled default JSON files only when the user's runtime data file
    # does not exist. This prevents EXE updates from overwriting old documents.
    try:
        if DEFAULT_DATA_DIR.exists() and DEFAULT_DATA_DIR.resolve() != DATA_DIR.resolve():
            for src in DEFAULT_DATA_DIR.glob("*.json"):
                dst = DATA_DIR / src.name
                if not dst.exists():
                    shutil.copy2(src, dst)
    except Exception:
        pass


def _timestamp():
    return datetime.now().strftime("%Y%m%d_%H%M%S")


def _backup_existing_file(path: Path, reason="save", keep=6):
    """Keep small rolling backups for JSON data files.

    This protects user documents from accidental corruption or bad imports while
    avoiding a huge backup folder. It is intentionally local-only.
    """
    try:
        if not path.exists() or not path.is_file():
            return
        BACKUP_DIR.mkdir(parents=True, exist_ok=True)
        target = BACKUP_DIR / f"{path.stem}_{reason}_{_timestamp()}{path.suffix}"
        shutil.copy2(path, target)
        old = sorted(BACKUP_DIR.glob(f"{path.stem}_*{path.suffix}"), key=lambda fp: fp.stat().st_mtime, reverse=True)
        for fp in old[keep:]:
            try:
                fp.unlink()
            except Exception:
                pass
    except Exception:
        pass


def load_json(path: Path, default):
    try:
        text = path.read_text(encoding="utf-8")
        _JSON_HASH_CACHE[str(path.resolve())] = _json_hash(text)
        return json.loads(text)
    except FileNotFoundError:
        return default
    except Exception:
        # If a runtime JSON is damaged, preserve it before falling back. This
        # makes the app open instead of getting stuck on a bad old document file.
        _backup_existing_file(path, reason="corrupt", keep=3)
        return default


def _should_backup_before_save(path: Path, current_hash: str, min_interval=45.0) -> bool:
    """Rate-limit backup copies so autosave cannot make the UI stutter.

    Saving is still atomic on every change; only the safety copy is throttled.
    This keeps editing smooth when documents.json is large or on slower disks.
    """
    key = str(path.resolve())
    if not path.exists():
        return False
    if _JSON_HASH_CACHE.get(key) == current_hash:
        return False
    now = time.monotonic()
    last = _LAST_BACKUP_AT.get(key, 0.0)
    if now - last >= min_interval:
        _LAST_BACKUP_AT[key] = now
        return True
    return False


def _json_save_text(path: Path, data) -> str:
    # User settings stay readable; high-frequency data files use compact JSON
    # to reduce disk IO and startup parse time.
    if path.name in {"settings.json", "account_spaces.json"}:
        return json.dumps(data, ensure_ascii=False, indent=2)
    return json.dumps(data, ensure_ascii=False, separators=(",", ":"))


def save_json(path: Path, data):
    """Atomically save JSON while avoiding needless repeated disk work.

    Stability: writes go through a temp file and are replaced atomically.
    Smoothness: unchanged payloads are skipped, compact data files are smaller,
    and backup copies are rate-limited during autosave bursts.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    text = _json_save_text(path, data)
    new_hash = _json_hash(text)
    key = str(path.resolve())
    if _JSON_HASH_CACHE.get(key) == new_hash and path.exists():
        return

    fd, tmp_name = tempfile.mkstemp(prefix=path.stem + "_", suffix=".tmp", dir=str(path.parent))
    tmp_path = Path(tmp_name)
    try:
        with open(fd, "w", encoding="utf-8") as f:
            f.write(text)
        if _should_backup_before_save(path, new_hash):
            _backup_existing_file(path, reason="before_save", keep=6)
        tmp_path.replace(path)
        _JSON_HASH_CACHE[key] = new_hash
    finally:
        try:
            if tmp_path.exists():
                tmp_path.unlink()
        except Exception:
            pass


def _settings_mode():
    """Return active data scope while avoiding repeated settings.json reads.

    v2026.06.20.17: autosave can call this many times.  Checking mtime is much
    cheaper than parsing settings.json on every note/task/document save, and it
    still updates immediately after settings are saved during mode switches.
    """
    path = DATA_DIR / "settings.json"
    try:
        mtime = path.stat().st_mtime if path.exists() else None
    except Exception:
        mtime = None
    if _SETTINGS_MODE_CACHE.get("mtime") == mtime:
        return _SETTINGS_MODE_CACHE.get("mode", "enterprise")
    settings = load_json(path, {})
    mode = settings.get("app_mode", "enterprise") if isinstance(settings, dict) else "enterprise"
    mode = mode if mode in ("personal", "enterprise") else "enterprise"
    _SETTINGS_MODE_CACHE.update({"mtime": mtime, "mode": mode})
    return mode


def _default_scope_data():
    return {
        "version": "v2026.06.19.03",
        "link": {
            "association": "puid_workspace_link",
            "personal_puid": "",
            "enterprise_puid": "",
            "workspace_puid": "",
        },
        "personal": {
            "notes": {},
            "todos": {"items": []},
            "documents": {},
        },
        "enterprise": {
            "notes": {},
            "todos": {"items": []},
            "documents": {},
        },
    }


def _normalize_scope_data(data):
    default = _default_scope_data()
    if not isinstance(data, dict):
        data = default
    data.setdefault("version", default["version"])
    data.setdefault("link", default["link"])
    for scope in ("personal", "enterprise"):
        data.setdefault(scope, {})
        data[scope].setdefault("notes", {})
        data[scope].setdefault("todos", {"items": []})
        data[scope].setdefault("documents", {})
        if not isinstance(data[scope].get("todos"), dict) or "items" not in data[scope].get("todos", {}):
            data[scope]["todos"] = {"items": []}
    return data


def load_scoped_workspace_data():
    ensure_data_dir()
    path = DATA_DIR / "scoped_workspace.json"
    data = _normalize_scope_data(load_json(path, _default_scope_data()))

    # First-run migration: keep old local files as enterprise data so existing
    # users do not lose anything, while personal starts cleanly separated.
    legacy_notes = load_json(DATA_DIR / "notes.json", {})
    legacy_todos = load_json(DATA_DIR / "todos.json", {"items": []})
    legacy_docs = load_json(DATA_DIR / "documents.json", {})
    if "items" not in legacy_todos:
        legacy_todos = {"items": []}
    if not path.exists() and (legacy_notes or legacy_todos.get("items") or legacy_docs):
        data["enterprise"]["notes"] = legacy_notes
        data["enterprise"]["todos"] = legacy_todos
        migrated_docs = {}
        for doc_id, doc in legacy_docs.items() if isinstance(legacy_docs, dict) else []:
            if isinstance(doc, dict):
                item = dict(doc)
                item.setdefault("scope", "enterprise")
                migrated_docs[doc_id] = item
            else:
                migrated_docs[doc_id] = doc
        data["enterprise"]["documents"] = migrated_docs
        save_json(path, data)
    return data


def save_scoped_workspace_data(data):
    save_json(DATA_DIR / "scoped_workspace.json", _normalize_scope_data(data))


def _load_scope_bucket(bucket, fallback):
    data = load_scoped_workspace_data()
    mode = _settings_mode()
    return data.get(mode, {}).get(bucket, fallback)


def _cached_link_metadata(max_age=180.0):
    """Return system link metadata without re-reading settings on every autosave.

    v2026.06.20.17: documents/tasks can autosave frequently.  Older code read
    settings.json and enterprise.json during every scoped save, which created
    unnecessary disk IO and made low-end PCs feel laggy.  A short cache is enough
    because these identifiers rarely change during one app session, and the data
    files still remain the source of truth.
    """
    now = time.monotonic()
    try:
        if now - float(_LINK_METADATA_CACHE.get("loaded_at", 0.0)) <= float(max_age):
            return dict(_LINK_METADATA_CACHE)
    except Exception:
        pass
    settings = load_json(DATA_DIR / "settings.json", {})
    enterprise = load_json(DATA_DIR / "enterprise.json", {})
    if not isinstance(settings, dict):
        settings = {}
    if not isinstance(enterprise, dict):
        enterprise = {}
    _LINK_METADATA_CACHE.update({
        "loaded_at": now,
        "personal_puid": settings.get("personal_puid", ""),
        "enterprise_puid": settings.get("enterprise_puid", ""),
        "workspace_puid": enterprise.get("workspace_puid", ""),
    })
    return dict(_LINK_METADATA_CACHE)


def _save_scope_bucket(bucket, value):
    data = load_scoped_workspace_data()
    mode = _settings_mode()
    data.setdefault(mode, {})
    data[mode][bucket] = value
    # Maintain association metadata from a short runtime cache instead of
    # re-reading settings/enterprise JSON on every document or task autosave.
    meta = _cached_link_metadata()
    data.setdefault("link", {})
    if meta.get("personal_puid"):
        data["link"]["personal_puid"] = meta.get("personal_puid", "")
    if meta.get("enterprise_puid"):
        data["link"]["enterprise_puid"] = meta.get("enterprise_puid", "")
    if meta.get("workspace_puid"):
        data["link"]["workspace_puid"] = meta.get("workspace_puid", "")
    save_scoped_workspace_data(data)


def load_data():
    ensure_data_dir()
    # v2026.06.20.17: load scoped workspace once.  Previous code loaded the
    # scoped workspace three times for notes/todos/documents, which is visible
    # on slower disks and when switching pages.
    scoped = load_scoped_workspace_data()
    mode = _settings_mode()
    bucket = scoped.get(mode, {}) if isinstance(scoped, dict) else {}
    notes = bucket.get("notes", {}) if isinstance(bucket, dict) else {}
    todos = bucket.get("todos", {"items": []}) if isinstance(bucket, dict) else {"items": []}
    documents = bucket.get("documents", {}) if isinstance(bucket, dict) else {}
    holidays = load_json(DATA_DIR / "holidays.json", {}).get("holidays", {})
    if not isinstance(notes, dict):
        notes = {}
    if not isinstance(todos, dict) or "items" not in todos:
        todos = {"items": []}
    if not isinstance(documents, dict):
        documents = {}
    return notes, todos, documents, holidays


def save_notes(notes):
    save_json(DATA_DIR / "notes.json", notes)  # legacy mirror for compatibility
    _save_scope_bucket("notes", notes)


def save_todos(todos):
    save_json(DATA_DIR / "todos.json", todos)  # legacy mirror for compatibility
    _save_scope_bucket("todos", todos)


def save_documents(documents):
    save_json(DATA_DIR / "documents.json", documents)  # legacy mirror for compatibility
    _save_scope_bucket("documents", documents)


def load_settings():
    default = {
        "username": "",
        "logged_in": False,
        "accounts": [],
        "puid": "",
        "personal_puid": "",
        "enterprise_puid": "",
        "quick_actions": {"calendar": True, "task": True, "note": True, "chat": True, "cloud": True, "enterprise": True, "ai": True},
        "quick_action_labels": {"calendar": "", "task": "", "note": "", "chat": "", "cloud": "", "enterprise": "", "ai": ""},
        "holiday_reminders": {"china": True, "indonesia": True, "days_before": 1},
        "theme": "blue",
        "app_mode": "enterprise",
        "remember_me": False,
        "auto_login": False,
        "saved_password": "",
        "chat_scope": "enterprise",
        "contact_card": {
            "display_name": "",
            "title": "",
            "account": "",
            "phone": "",
            "email": "",
            "bio": "",
            "avatar_path": "",
            "public_contact_code": "",
        },
        "server_sync": {
            "enabled": False,
            "offline_mode": True,
            "base_url": "",
            "workspace_id": "",
            "api_token": "",
            "timeout": 5,
            "retry_count": 1,
            "allow_insecure_localhost": True,
            "managed_by_system": True,
            "user_configurable": False,
            "config_source": "not_configured",
            "auto_connect": False,
            "sync_events_enabled": False,
            "deployment_mode": "local"
        }
    }
    data = load_json(DATA_DIR / "settings.json", default)
    if not isinstance(data, dict):
        return default
    default.update(data)
    if not isinstance(default.get("contact_card"), dict):
        default["contact_card"] = {
            "display_name": "",
            "title": "",
            "account": "",
            "phone": "",
            "email": "",
            "bio": "",
            "avatar_path": "",
            "public_contact_code": "",
        }
    else:
        for key in ("display_name", "title", "account", "phone", "email", "bio", "avatar_path", "public_contact_code"):
            default["contact_card"].setdefault(key, "")
    default.setdefault("performance_tier", "auto")
    default.setdefault("chat_scope", "enterprise")
    default.setdefault("close_to_tray", True)
    default.setdefault("source_mode_close_direct", True)
    default.setdefault("source_mode_force_os_exit", True)
    server_sync = default.setdefault("server_sync", {})
    if not isinstance(server_sync, dict):
        server_sync = {}
        default["server_sync"] = server_sync
    server_sync.setdefault("retry_count", 1)
    server_sync.setdefault("managed_by_system", True)
    server_sync.setdefault("user_configurable", False)
    server_sync.setdefault("config_source", "not_configured")
    server_sync.setdefault("auto_connect", False)
    server_sync.setdefault("sync_events_enabled", False)
    server_sync.setdefault("deployment_mode", "local")
    return default


def save_settings(settings):
    save_json(DATA_DIR / "settings.json", settings)


def default_enterprise_data():
    return {
        "employees": [
            {
                "id": "emp-admin",
                "name": "Admin",
                "position": "Manager",
                "department": "Management",
                "email": "admin@example.com",
                "is_admin": True,
                "status": "active",
            }
        ],
        "notifications": [],
        "last_sync": "",
        "company_name": "",
        "company_created": False,
        "workspace_puid": "",
        "goals": [],
    }


def load_enterprise():
    data = load_json(DATA_DIR / "enterprise.json", default_enterprise_data())
    if not isinstance(data, dict):
        data = default_enterprise_data()
    data.setdefault("employees", [])
    data.setdefault("notifications", [])
    data.setdefault("last_sync", "")
    data.setdefault("company_name", "")
    data.setdefault("workspace_puid", "")
    data.setdefault("goals", [])
    if "company_created" not in data:
        data["company_created"] = bool(data.get("company_name"))
    return data


def save_enterprise(data):
    save_json(DATA_DIR / "enterprise.json", data)


def default_crm_data():
    return {
        "customers": [],
        "tickets": [],
        "activities": [],
        "last_sync": "",
    }


def load_crm():
    data = load_json(DATA_DIR / "crm.json", default_crm_data())
    if not isinstance(data, dict):
        data = default_crm_data()
    data.setdefault("customers", [])
    data.setdefault("tickets", [])
    data.setdefault("activities", [])
    data.setdefault("last_sync", "")
    return data


def save_crm(data):
    save_json(DATA_DIR / "crm.json", data)


def default_chat_data():
    return {
        "threads": {"company-group": []},
        "enterprise_threads": {"company-group": []},
        "personal_threads": {"personal-notes": [], "personal-todo": []},
        "personal_friends": [],
        "enterprise_friends": [],
        "personal_groups": [],
        "enterprise_groups": [],
        "space_meta": {
            "personal": {"model": "wechat_like", "linked": True},
            "enterprise": {"model": "wecom_like", "linked": True},
        },
    }


def load_chat():
    data = load_json(DATA_DIR / "chat.json", default_chat_data())
    if not isinstance(data, dict):
        data = default_chat_data()
    data.setdefault("threads", {"company-group": []})
    data.setdefault("enterprise_threads", data.get("threads", {"company-group": []}))
    data.setdefault("personal_threads", {"personal-notes": [], "personal-todo": []})
    data.setdefault("personal_friends", [])
    data.setdefault("enterprise_friends", [])
    data.setdefault("personal_groups", [])
    data.setdefault("enterprise_groups", [])
    data.setdefault("space_meta", {"personal": {"model": "wechat_like", "linked": True}, "enterprise": {"model": "wecom_like", "linked": True}})
    data["enterprise_threads"].setdefault("company-group", [])
    data["personal_threads"].setdefault("personal-notes", [])
    data["personal_threads"].setdefault("personal-todo", [])
    return data


def save_chat(data):
    save_json(DATA_DIR / "chat.json", data)



def load_action_logs():
    data = load_json(DATA_DIR / "action_logs.json", {"items": []})
    if not isinstance(data, dict):
        data = {"items": []}
    items = data.get("items", [])
    if not isinstance(items, list):
        items = []
    data["items"] = items[-300:]
    return data


def save_action_logs(data):
    items = data.get("items", []) if isinstance(data, dict) else []
    if not isinstance(items, list):
        items = []
    save_json(DATA_DIR / "action_logs.json", {"items": items[-300:]})


def load_reports():
    data = load_json(DATA_DIR / "reports.json", {"items": []})
    if not isinstance(data, dict):
        data = {"items": []}
    items = data.get("items", [])
    if not isinstance(items, list):
        items = []
    data["items"] = items[-500:]
    return data

def save_reports(data):
    items = data.get("items", []) if isinstance(data, dict) else []
    if not isinstance(items, list):
        items = []
    save_json(DATA_DIR / "reports.json", {"items": items[-500:]})

def load_attendance():
    data = load_json(DATA_DIR / "attendance.json", {"items": []})
    if not isinstance(data, dict):
        data = {"items": []}
    items = data.get("items", [])
    if not isinstance(items, list):
        items = []
    data["items"] = items[-800:]
    return data

def save_attendance(data):
    items = data.get("items", []) if isinstance(data, dict) else []
    if not isinstance(items, list):
        items = []
    save_json(DATA_DIR / "attendance.json", {"items": items[-800:]})
