from __future__ import annotations

import gzip
import json
import hashlib
import re
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional

from core.config import APP_DATA_DIR, logger
from core.db import SyncJSONEncoder
from core.profile_sync_service import ProfileSyncService, PROFILE_SYNC_PRIORITY_TABLES
from core.tenant_manager import TenantManager

MANUAL_BACKUP_DAILY_LIMIT = 3
MANUAL_RESTORE_DAILY_LIMIT = 3
QUOTA_API_URL = "https://app.golder.biz/api/backup/quota.php"

BACKUP_SCHEMA_VERSION = "golder-backup-v2"
PROFILE_SNAPSHOT_SCHEMA = "profile-sync-v1"


def _get_cloud_quota() -> Dict[str, Any]:
    """
    Fetch daily quota usage from the web API.
    Returns empty dict if unavailable (offline fallback).
    """
    try:
        import json as _json
        import ssl
        import urllib.request

        tm = TenantManager()
        token = tm.get_api_token()
        license_data = tm.load_license()
        customer_id = str(license_data.get("customer_id") or "").strip()
        if not token or not customer_id:
            return {}

        payload = _json.dumps({"action": "get"}).encode("utf-8")
        req = urllib.request.Request(
            QUOTA_API_URL,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {token}",
            },
            method="POST",
        )
        ctx = ssl.create_default_context()
        with urllib.request.urlopen(req, timeout=5, context=ctx) as resp:
            data = _json.loads(resp.read().decode("utf-8"))
            if data.get("status") == "ok":
                return data
    except Exception:
        pass
    return {}


def _is_safe_identifier(name: str) -> bool:
    return bool(name) and name.replace("_", "").isalnum()


def _utc_now() -> str:
    return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")


def _canonicalize(value: Any) -> Any:
    if isinstance(value, dict):
        return {k: _canonicalize(value[k]) for k in sorted(value.keys(), key=lambda x: str(x))}
    if isinstance(value, list):
        return [_canonicalize(v) for v in value]
    return value


def canonical_json(payload: Dict[str, Any]) -> str:
    normalized = _canonicalize(payload)
    return json.dumps(
        normalized,
        cls=SyncJSONEncoder,
        ensure_ascii=False,
        separators=(",", ":"),
    )


def backup_storage_dir() -> Path:
    target = APP_DATA_DIR / "backups"
    target.mkdir(parents=True, exist_ok=True)
    return target


def _sanitize_slug(value: str) -> str:
    cleaned = re.sub(r"[^a-zA-Z0-9_-]+", "-", str(value or "").strip())
    return cleaned.strip("-_") or "profile"


def _backup_filename(primary_profile_id: str, backup_type: str, profile_count: int) -> str:
    base_name = "multi" if int(profile_count or 0) > 1 else _sanitize_slug(primary_profile_id)
    return f"{datetime.utcnow():%Y%m%d_%H%M%S}_{base_name}_{backup_type}.golder"


def default_backup_filename(primary_profile_id: str, profile_count: int = 1, backup_type: str = "manual") -> str:
    return _backup_filename(primary_profile_id, backup_type, profile_count)


def manual_backup_daily_count() -> int:
    cloud = _get_cloud_quota()
    if cloud:
        return int(cloud.get("backup_used", 0))
    backup_dir = backup_storage_dir()
    prefix = datetime.utcnow().strftime("%Y%m%d_")
    return len([p for p in backup_dir.glob(f"{prefix}*_manual.golder") if p.is_file()])


def manual_restore_daily_count() -> int:
    cloud = _get_cloud_quota()
    if cloud:
        return int(cloud.get("restore_used", 0))
    log_path = APP_DATA_DIR / "restore_log.json"
    today = datetime.utcnow().strftime("%Y-%m-%d")
    try:
        if log_path.is_file():
            data = json.loads(log_path.read_text(encoding="utf-8"))
            return int(data.get(today, 0))
    except Exception:
        pass
    return 0


def _increment_local_restore_count() -> None:
    log_path = APP_DATA_DIR / "restore_log.json"
    today = datetime.utcnow().strftime("%Y-%m-%d")
    data: Dict[str, Any] = {}
    try:
        if log_path.is_file():
            data = json.loads(log_path.read_text(encoding="utf-8"))
    except Exception:
        pass
    data[today] = int(data.get(today, 0)) + 1
    try:
        log_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
    except Exception:
        pass


def _list_backups_for(base_name: str, backup_type: str) -> List[Path]:
    backup_dir = backup_storage_dir()
    pattern = f"*_{base_name}_{backup_type}.golder"
    return sorted([p for p in backup_dir.glob(pattern) if p.is_file()], reverse=True)


def trim_backup_retention(base_name: str, backup_type: str, keep: int = 30) -> int:
    keep = max(1, int(keep or 30))
    rows = _list_backups_for(base_name, backup_type)
    deleted = 0
    for path in rows[keep:]:
        try:
            path.unlink()
            deleted += 1
        except Exception:
            logger.warning("Backup retention delete failed: %s", path)
    return deleted


def list_all_backups(limit: int = 30) -> List[Dict[str, Any]]:
    """
    List all .golder backups on disk.
    Each row includes: file_path, file_name, backup_type, profile_names,
    profile_count, file_size_kb, created_at.
    """
    backup_dir = backup_storage_dir()
    files = sorted(
        [p for p in backup_dir.glob("*.golder") if p.is_file()],
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )

    rows: List[Dict[str, Any]] = []
    for path in files[:max(1, int(limit or 30))]:
        name = path.stem
        parts = name.split("_")

        backup_type = "manual"
        if parts and parts[-1] in ("manual", "auto"):
            backup_type = parts[-1]

        profile_slug = ""
        if len(parts) >= 3:
            profile_slug = "_".join(parts[2:-1]) if len(parts) > 3 else (parts[2] if len(parts) > 2 else "")

        profile_names = profile_slug or "-"
        profile_count = 1
        try:
            bundle = _read_backup_bundle(str(path))
            manifest = bundle.get("manifest", {})
            profiles_in_bundle = manifest.get("profiles", [])
            profile_count = max(1, int(manifest.get("profile_count") or len(profiles_in_bundle) or 1))
            names = [
                str(p.get("profile_name") or p.get("profile_id") or "").strip()
                for p in profiles_in_bundle
                if isinstance(p, dict)
            ]
            names = [n for n in names if n]
            if names:
                profile_names = ", ".join(names)
        except Exception:
            pass

        try:
            file_size_bytes = path.stat().st_size
        except Exception:
            file_size_bytes = 0

        try:
            mtime = path.stat().st_mtime
            created_at = datetime.utcfromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
        except Exception:
            created_at = "-"

        rows.append(
            {
                "file_path": str(path),
                "file_name": path.name,
                "backup_type": backup_type,
                "profile_names": profile_names,
                "profile_count": profile_count,
                "file_size_bytes": file_size_bytes,
                "file_size_kb": round(file_size_bytes / 1024, 2),
                "created_at": created_at,
            }
        )

    return rows


def create_backup_bundle(
    db,
    profile_ids: Iterable[str],
    backup_type: str = "manual",
    target_path: Optional[str] = None,
) -> Dict[str, Any]:
    profile_ids = [str(pid or "").strip() for pid in profile_ids or [] if str(pid or "").strip()]
    if not profile_ids:
        return {"ok": False, "message": "At least one profile must be selected."}
    if backup_type == "manual":
        used = manual_backup_daily_count()
        if used >= MANUAL_BACKUP_DAILY_LIMIT:
            return {
                "ok": False,
                "message": f"Günlük yedek limitine ulaşıldı ({MANUAL_BACKUP_DAILY_LIMIT}/gün).",
            }

    sync_service = ProfileSyncService(db)
    profiles: List[Dict[str, Any]] = []
    for profile_id in profile_ids:
        snapshot = sync_service._export_local_profile(profile_id)
        snapshot_json = canonical_json(snapshot)
        profiles.append(
            {
                "profile_id": str(snapshot.get("profile", {}).get("id") or profile_id),
                "profile_name": str(snapshot.get("profile", {}).get("name") or ""),
                "snapshot_schema_version": PROFILE_SNAPSHOT_SCHEMA,
                "snapshot_checksum_sha256": hashlib.sha256(snapshot_json.encode("utf-8")).hexdigest(),
                "snapshot": snapshot,
            }
        )

    try:
        customer_id = int(TenantManager().load_license().get("customer_id") or 0)
    except Exception:
        customer_id = 0

    manifest: Dict[str, Any] = {
        "schema_version": BACKUP_SCHEMA_VERSION,
        "created_at_utc": _utc_now(),
        "backup_type": str(backup_type or "manual"),
        "customer_id": customer_id,
        "profile_count": len(profiles),
        "profiles": profiles,
    }

    manifest_json = canonical_json(manifest)
    checksum = hashlib.sha256(manifest_json.encode("utf-8")).hexdigest()
    payload = {
        "manifest": json.loads(manifest_json),
        "checksum_sha256": checksum,
    }
    payload_json = canonical_json(payload)
    compressed = gzip.compress(payload_json.encode("utf-8"), compresslevel=9)

    if target_path:
        path = Path(str(target_path))
        if path.suffix.lower() != ".golder":
            path = path.with_suffix(".golder")
    else:
        filename = _backup_filename(profile_ids[0], backup_type, len(profiles))
        path = backup_storage_dir() / filename

    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(compressed)
    except Exception as exc:
        return {"ok": False, "message": f"Backup file write failed: {exc}"}

    base_name = "multi" if len(profiles) > 1 else _sanitize_slug(profile_ids[0])
    trim_backup_retention(base_name, backup_type, keep=30)

    return {
        "ok": True,
        "file_path": str(path),
        "checksum_sha256": checksum,
        "file_size": path.stat().st_size if path.exists() else 0,
        "profile_count": len(profiles),
        "profiles": profiles,
    }


def _read_backup_bundle(file_path: str) -> Dict[str, Any]:
    path = Path(file_path)
    if not path.is_file():
        raise RuntimeError("Backup file not found.")
    raw = path.read_bytes()
    try:
        payload = json.loads(gzip.decompress(raw).decode("utf-8"))
    except Exception as exc:
        raise RuntimeError(f"Backup file decode failed: {exc}") from exc

    manifest = payload.get("manifest")
    checksum = str(payload.get("checksum_sha256") or "").strip()
    if not isinstance(manifest, dict):
        raise RuntimeError("Backup manifest is invalid.")
    if not checksum:
        raise RuntimeError("Backup checksum is missing.")

    manifest_json = canonical_json(manifest)
    expected = hashlib.sha256(manifest_json.encode("utf-8")).hexdigest()
    if checksum != expected:
        raise RuntimeError("Backup checksum mismatch.")

    if str(manifest.get("schema_version") or "") != BACKUP_SCHEMA_VERSION:
        raise RuntimeError("Unsupported backup schema version.")

    return {"manifest": manifest, "checksum_sha256": checksum}


def _order_profile_tables(table_names: Iterable[str]) -> List[str]:
    remaining = {str(name) for name in table_names if name}
    ordered: List[str] = []
    for name in PROFILE_SYNC_PRIORITY_TABLES:
        if name in remaining:
            ordered.append(name)
            remaining.remove(name)
    ordered.extend(sorted(remaining))
    return ordered


def _table_columns(db, table_name: str) -> Dict[str, bool]:
    if not _is_safe_identifier(table_name):
        return {}
    rows = db.fetch_all(
        """
        SELECT COLUMN_NAME
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = %s
        """,
        (table_name,),
    ) or []
    cols: Dict[str, bool] = {}
    for row in rows:
        name = str(row.get("COLUMN_NAME") or "").strip()
        if name and _is_safe_identifier(name):
            cols[name] = True
    return cols


def _backup_walk_remap(value: Any, id_map: Dict[str, str]) -> Any:
    if isinstance(value, list):
        return [_backup_walk_remap(v, id_map) for v in value]
    if isinstance(value, dict):
        return {k: _backup_walk_remap(v, id_map) for k, v in value.items()}
    if isinstance(value, str):
        trimmed = value.strip()
        if trimmed and trimmed in id_map:
            return id_map[trimmed]
        try:
            decoded = json.loads(value)
        except Exception:
            return value
        if isinstance(decoded, (dict, list)):
            remapped = _backup_walk_remap(decoded, id_map)
            return json.dumps(remapped, ensure_ascii=False, separators=(",", ":"))
    return value


def _insert_row(cursor, table_name: str, row: Dict[str, Any]) -> None:
    if not _is_safe_identifier(table_name) or not row:
        return
    columns = list(row.keys())
    placeholders = ", ".join(["%s"] * len(columns))
    column_sql = ", ".join([f"`{col}`" for col in columns])
    sql = f"INSERT INTO `{table_name}` ({column_sql}) VALUES ({placeholders})"
    cursor.execute(sql, tuple(row[col] for col in columns))


def _resolve_profile_name(db, base_name: str, date_label: str) -> str:
    base_name = str(base_name or "").strip() or "Profil"
    row = db.fetch_one(
        "SELECT COUNT(*) AS cnt FROM profiles WHERE name = %s AND COALESCE(is_deleted, 0) = 0",
        (base_name,),
    ) or {}
    if int(row.get("cnt") or 0) == 0:
        return base_name

    candidate = f"{base_name} ({date_label} yedek)"
    row = db.fetch_one(
        "SELECT COUNT(*) AS cnt FROM profiles WHERE name = %s AND COALESCE(is_deleted, 0) = 0",
        (candidate,),
    ) or {}
    if int(row.get("cnt") or 0) == 0:
        return candidate

    for idx in range(2, 1000):
        next_name = f"{base_name} ({date_label} yedek {idx})"
        row = db.fetch_one(
            "SELECT COUNT(*) AS cnt FROM profiles WHERE name = %s AND COALESCE(is_deleted, 0) = 0",
            (next_name,),
        ) or {}
        if int(row.get("cnt") or 0) == 0:
            return next_name

    return f"{candidate} {str(uuid.uuid4())[:8]}"


def _restore_snapshot_as_new_profile(db, snapshot: Dict[str, Any], date_label: str) -> Dict[str, Any]:
    profile_row = snapshot.get("profile") or {}
    source_profile_id = str(profile_row.get("id") or "").strip()
    source_profile_name = str(profile_row.get("name") or "").strip()
    if not source_profile_id:
        raise RuntimeError("Snapshot profile id is required.")

    new_profile_id = str(uuid.uuid4())
    new_profile_name = _resolve_profile_name(db, source_profile_name or "Profil", date_label)
    id_map: Dict[str, str] = {source_profile_id: new_profile_id}

    tables = snapshot.get("tables") or {}
    related = snapshot.get("related") or {}

    for rows in tables.values():
        if not isinstance(rows, list):
            continue
        for row in rows:
            if not isinstance(row, dict):
                continue
            old_id = str(row.get("id") or "").strip()
            if old_id and old_id not in id_map:
                id_map[old_id] = str(uuid.uuid4())

    for table_name in ("user_permissions", "user_access_scopes"):
        for row in related.get(table_name) or []:
            if not isinstance(row, dict):
                continue
            old_id = str(row.get("id") or "").strip()
            if old_id and old_id not in id_map:
                id_map[old_id] = str(uuid.uuid4())

    with db.transaction_scope() as cursor:
        try:
            cursor.execute("SET FOREIGN_KEY_CHECKS=0")
        except Exception:
            pass

        profile_cols = _table_columns(db, "profiles")
        prepared_profile = {
            k: _backup_walk_remap(v, id_map)
            for k, v in profile_row.items()
            if k in profile_cols
        }
        prepared_profile["id"] = new_profile_id
        if "name" in prepared_profile:
            prepared_profile["name"] = new_profile_name
        if "is_deleted" in prepared_profile:
            prepared_profile["is_deleted"] = 0
        _insert_row(cursor, "profiles", prepared_profile)

        for table_name in _order_profile_tables(tables.keys()):
            rows = tables.get(table_name) or []
            if not isinstance(rows, list) or not _is_safe_identifier(table_name):
                continue
            columns_map = _table_columns(db, table_name)
            if not columns_map:
                continue
            for row in rows:
                if not isinstance(row, dict):
                    continue
                prepared: Dict[str, Any] = {}
                for key, value in row.items():
                    if key in columns_map:
                        prepared[key] = _backup_walk_remap(value, id_map)
                if "profile_id" in columns_map:
                    prepared["profile_id"] = new_profile_id
                if "id" in columns_map:
                    old_id = str(row.get("id") or "").strip()
                    if old_id:
                        prepared["id"] = id_map.get(old_id, str(uuid.uuid4()))
                for column, value in list(prepared.items()):
                    if column == "profile_id":
                        continue
                    if (column == "related_id" or column.endswith("_id")) and isinstance(value, str):
                        lookup = value.strip()
                        if lookup and lookup in id_map:
                            prepared[column] = id_map[lookup]
                _insert_row(cursor, table_name, prepared)

        for table_name in ("user_permissions", "user_access_scopes"):
            rows = related.get(table_name) or []
            if not isinstance(rows, list) or not rows:
                continue
            columns_map = _table_columns(db, table_name)
            if not columns_map:
                continue
            for row in rows:
                if not isinstance(row, dict):
                    continue
                prepared: Dict[str, Any] = {}
                for key, value in row.items():
                    if key in columns_map:
                        prepared[key] = _backup_walk_remap(value, id_map)
                if "id" in columns_map:
                    old_id = str(row.get("id") or "").strip()
                    if old_id:
                        prepared["id"] = id_map.get(old_id, str(uuid.uuid4()))
                if "user_id" in columns_map:
                    old_user_id = str(row.get("user_id") or "").strip()
                    if old_user_id and old_user_id in id_map:
                        prepared["user_id"] = id_map[old_user_id]
                _insert_row(cursor, table_name, prepared)

        try:
            cursor.execute("SET FOREIGN_KEY_CHECKS=1")
        except Exception:
            pass

    return {
        "source_profile_id": source_profile_id,
        "profile_id": new_profile_id,
        "profile_name": new_profile_name,
    }


def restore_backup_bundle(db, file_path: str) -> Dict[str, Any]:
    used = manual_restore_daily_count()
    if used >= MANUAL_RESTORE_DAILY_LIMIT:
        return {
            "ok": False,
            "message": f"Günlük geri yükleme limitine ulaşıldı ({MANUAL_RESTORE_DAILY_LIMIT}/gün).",
        }

    bundle = _read_backup_bundle(file_path)
    manifest = bundle["manifest"]
    restored: List[Dict[str, Any]] = []
    date_label = datetime.utcnow().strftime("%Y-%m-%d")
    for profile_item in manifest.get("profiles") or []:
        if not isinstance(profile_item, dict):
            continue
        snapshot = profile_item.get("snapshot") or {}
        if not isinstance(snapshot, dict):
            continue
        restored.append(_restore_snapshot_as_new_profile(db, snapshot, date_label))

    _increment_local_restore_count()

    return {
        "ok": True,
        "restored_profiles": restored,
        "profile_count": len(restored),
    }


def maybe_run_auto_backup(db, profile_id: str) -> bool:
    tenant = TenantManager()
    if not tenant.can_use_backup_features():
        return False
    profile_id = str(profile_id or "").strip()
    if not profile_id:
        return False
    backup_dir = backup_storage_dir()
    date_prefix = datetime.utcnow().strftime("%Y%m%d_")
    base_name = _sanitize_slug(profile_id)
    already = list(backup_dir.glob(f"{date_prefix}*_{base_name}_auto.golder"))
    if already:
        return False
    result = create_backup_bundle(db, [profile_id], backup_type="auto")
    return bool(result.get("ok"))




