import io
import json
import logging
import os
import shutil
import subprocess
import tempfile
from datetime import datetime
from urllib.parse import urlparse
from zipfile import ZIP_DEFLATED, ZipFile

logger = logging.getLogger(__name__)


def get_backup_root_dir(app) -> str:
    """Return the directory where backup archives should be stored.

    Priority:
    1) app.config["BACKUP_FOLDER"] (or env BACKUP_FOLDER / BACKUP_DIR)
    2) <UPLOAD_FOLDER>/backups (UPLOAD_FOLDER defaults to /data/uploads)

    The directory is created if missing.
    """
    # Prefer explicit backup folder if configured
    configured = (
        (app.config.get("BACKUP_FOLDER") if getattr(app, "config", None) else None)
        or os.getenv("BACKUP_FOLDER")
        or os.getenv("BACKUP_DIR")
    )
    if configured:
        backups_dir = os.path.abspath(str(configured))
    else:
        upload_root = (app.config.get("UPLOAD_FOLDER") if getattr(app, "config", None) else None) or os.getenv(
            "UPLOAD_FOLDER", "/data/uploads"
        )
        backups_dir = os.path.abspath(os.path.join(str(upload_root), "backups"))

    # Ensure the directory exists and is writable
    try:
        os.makedirs(backups_dir, exist_ok=True)
    except PermissionError as e:
        raise PermissionError(
            f"Permission denied creating backups directory '{backups_dir}'. "
            "Set BACKUP_FOLDER (or BACKUP_DIR) to a writable path or fix volume permissions."
        ) from e

    if not os.access(backups_dir, os.W_OK):
        raise PermissionError(
            f"Backups directory '{backups_dir}' is not writable. "
            "Set BACKUP_FOLDER (or BACKUP_DIR) to a writable path or fix volume permissions."
        )

    return backups_dir


def _get_backup_root_dir(app):
    """Backward-compatible wrapper for internal callers."""
    return get_backup_root_dir(app)


def _now_timestamp():
    """Return a human-readable local timestamp for file names."""
    # Respect user's preference to use local time across the project
    # rather than UTC for user-facing timestamps.
    return datetime.now().strftime("%Y%m%d_%H%M%S")


def _detect_db_type_and_path(app):
    """Detect database type and return a tuple (type, uri, sqlite_path).

    type: 'sqlite' or 'postgresql'
    uri: full SQLAlchemy database URI
    sqlite_path: file path if sqlite, otherwise None
    """
    uri = app.config.get("SQLALCHEMY_DATABASE_URI", "") or ""
    if isinstance(uri, str) and uri.startswith("sqlite:///"):
        return "sqlite", uri, uri.replace("sqlite:///", "")
    if isinstance(uri, str) and (uri.startswith("postgresql") or uri.startswith("postgres")):
        return "postgresql", uri, None
    # Default/fallback
    return "unknown", uri, None


def _get_alembic_revision(db_session):
    """Return current alembic revision string or None if unavailable."""
    try:
        from sqlalchemy import text

        result = db_session.execute(text("SELECT version_num FROM alembic_version"))
        row = result.first()
        return row[0] if row else None
    except Exception as e:
        logger.warning("Could not read alembic revision: %s", e)
        return None


def _write_manifest(zf, manifest: dict):
    data = json.dumps(manifest, indent=2, sort_keys=True).encode("utf-8")
    zf.writestr("manifest.json", data)


def _add_directory_to_zip(zf, source_dir: str, arc_prefix: str):
    if not source_dir or not os.path.isdir(source_dir):
        return
    for root, _, files in os.walk(source_dir):
        for file_name in files:
            abs_path = os.path.join(root, file_name)
            rel_path = os.path.relpath(abs_path, start=source_dir)
            zf.write(abs_path, os.path.join(arc_prefix, rel_path))


def create_backup(app) -> str:
    """Create a comprehensive backup archive and return its absolute path.

    Contents:
    - manifest.json (metadata: created_at, db_type, alembic_revision)
    - db.sqlite (if sqlite) OR db.dump (if postgresql, custom format)
    - settings.json (serialized Settings row for quick inspection)
    - uploads/ (logos and other uploaded assets if present)
    """
    # Late imports to avoid circular dependencies
    from app import db
    from app.models.settings import Settings

    backups_dir = _get_backup_root_dir(app)
    timestamp = _now_timestamp()
    archive_name = f"timetracker_backup_{timestamp}.zip"
    archive_path = os.path.join(backups_dir, archive_name)

    db_type, db_uri, sqlite_path = _detect_db_type_and_path(app)

    # Prepare temporary directory for DB dumps if needed
    tmp_dir = tempfile.mkdtemp(prefix="tt_backup_")
    tmp_db_artifact = None

    try:
        # Create DB artifact
        if db_type == "sqlite" and sqlite_path and os.path.exists(sqlite_path):
            tmp_db_artifact = os.path.join(tmp_dir, "db.sqlite")
            shutil.copy2(sqlite_path, tmp_db_artifact)
        elif db_type == "postgresql":
            # Use parsed connection parameters (avoid SQLAlchemy driver suffix in URI)
            database_url = os.getenv("DATABASE_URL", db_uri)
            parsed = urlparse(database_url) if database_url else None
            host = parsed.hostname if parsed and parsed.hostname else os.getenv("POSTGRES_HOST", "db")
            port = parsed.port if parsed and parsed.port else int(os.getenv("POSTGRES_PORT", "5432"))
            user = parsed.username if parsed and parsed.username else os.getenv("POSTGRES_USER", "timetracker")
            password = parsed.password if parsed and parsed.password else os.getenv("POSTGRES_PASSWORD", "timetracker")
            dbname = parsed.path.lstrip("/") if parsed and parsed.path else os.getenv("POSTGRES_DB", "timetracker")

            tmp_db_artifact = os.path.join(tmp_dir, "db.dump")
            pg_dump_cmd = [
                "pg_dump",
                "--format=custom",
                "-h",
                host,
                "-p",
                str(port),
                "-U",
                user,
                "-d",
                dbname,
                f"--file={tmp_db_artifact}",
            ]
            env = os.environ.copy()
            if password:
                env["PGPASSWORD"] = str(password)
            try:
                completed = subprocess.run(
                    pg_dump_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
                )
            except FileNotFoundError:
                raise RuntimeError("pg_dump not found. Please ensure PostgreSQL client tools are installed.")
            except subprocess.CalledProcessError as e:
                stderr = e.stderr.decode("utf-8", errors="ignore") if e.stderr else ""
                raise RuntimeError(f"pg_dump failed: {stderr.strip() or e}")
        else:
            # Best effort: we continue without DB artifact
            tmp_db_artifact = None

        # Gather metadata
        alembic_rev = _get_alembic_revision(db.session)
        manifest = {
            "created_at": datetime.now().isoformat(timespec="seconds"),
            "db_type": db_type,
            "alembic_revision": alembic_rev,
            "app_version": None,
        }

        # Serialize settings for convenience (DB backup still authoritative)
        settings_obj = Settings.get_settings()
        settings_json = json.dumps(settings_obj.to_dict(), indent=2, sort_keys=True)

        # Write the zip
        with ZipFile(archive_path, mode="w", compression=ZIP_DEFLATED) as zf:
            _write_manifest(zf, manifest)
            if tmp_db_artifact and os.path.exists(tmp_db_artifact):
                arc_name = "db.sqlite" if db_type == "sqlite" else "db.dump"
                zf.write(tmp_db_artifact, arc_name)

            zf.writestr("settings.json", settings_json.encode("utf-8"))

            # Include uploads (e.g., logos)
            uploads_root = os.path.join(app.root_path, "static", "uploads")
            _add_directory_to_zip(zf, uploads_root, "uploads")

        return archive_path
    finally:
        try:
            shutil.rmtree(tmp_dir, ignore_errors=True)
        except Exception as e:
            logger.debug("Backup temp dir cleanup failed: %s", e)


def restore_backup(app, archive_path: str, progress_callback=None) -> tuple[bool, str]:
    """Restore a backup archive.

    Steps:
    - Extract archive to temp dir
    - Restore DB depending on type
    - Copy uploads back
    - Run migrations to head to ensure compatibility with newer code

    Returns: (success, message)
    """
    from time import sleep

    from app import db

    if not archive_path or not os.path.exists(archive_path):
        return False, f"Backup archive not found: {archive_path}"

    db_type, db_uri, sqlite_path = _detect_db_type_and_path(app)
    tmp_dir = tempfile.mkdtemp(prefix="tt_restore_")

    def _progress(label: str, percent: int):
        try:
            if callable(progress_callback):
                progress_callback(label, percent)
        except Exception as e:
            # Log but continue - progress callback failure is not critical
            import logging

            logger = logging.getLogger(__name__)
            logger.debug(f"Progress callback failed: {e}")

    try:
        # Extract archive
        with ZipFile(archive_path, mode="r") as zf:
            zf.extractall(tmp_dir)
        _progress("Archive extracted", 10)

        # Read manifest (optional)
        manifest_path = os.path.join(tmp_dir, "manifest.json")
        if os.path.exists(manifest_path):
            try:
                with open(manifest_path, "r", encoding="utf-8") as f:
                    _ = json.load(f)
            except Exception as e:
                # Log but continue - manifest reading failure is not critical
                import logging

                logger = logging.getLogger(__name__)
                logger.debug(f"Failed to read backup manifest: {e}")

        # Proactively close any open DB connections before modifying files
        try:
            with app.app_context():
                db.session.remove()
                db.engine.dispose()
        except Exception:
            pass

        # Restore DB
        if db_type == "sqlite":
            src_sqlite = os.path.join(tmp_dir, "db.sqlite")
            if not os.path.exists(src_sqlite):
                return False, "Backup does not contain db.sqlite for SQLite restore"

            if not sqlite_path:
                return False, "Current configuration is not SQLite or path not found"

            # Ensure destination directory exists
            dest_dir = os.path.dirname(sqlite_path)
            if dest_dir and not os.path.exists(dest_dir):
                os.makedirs(dest_dir, exist_ok=True)

            # Safety copy of current DB if exists
            if os.path.exists(sqlite_path):
                safety_copy = sqlite_path + f".bak_{_now_timestamp()}"
                shutil.copy2(sqlite_path, safety_copy)

            # Replace DB file
            os.makedirs(os.path.dirname(sqlite_path), exist_ok=True)
            # Retry a few times in case the file is briefly locked
            last_err = None
            for _ in range(3):
                try:
                    shutil.copy2(src_sqlite, sqlite_path)
                    last_err = None
                    break
                except Exception as e:
                    last_err = e
                    sleep(0.2)
            if last_err:
                return False, f"Failed to write SQLite database file: {last_err}"
            _progress("SQLite database restored", 60)

        elif db_type == "postgresql":
            src_dump = os.path.join(tmp_dir, "db.dump")
            if not os.path.exists(src_dump):
                return False, "Backup does not contain db.dump for PostgreSQL restore"

            database_url = os.getenv("DATABASE_URL", db_uri)
            parsed = urlparse(database_url) if database_url else None
            host = parsed.hostname if parsed and parsed.hostname else os.getenv("POSTGRES_HOST", "db")
            port = parsed.port if parsed and parsed.port else int(os.getenv("POSTGRES_PORT", "5432"))
            user = parsed.username if parsed and parsed.username else os.getenv("POSTGRES_USER", "timetracker")
            password = parsed.password if parsed and parsed.password else os.getenv("POSTGRES_PASSWORD", "timetracker")
            dbname = parsed.path.lstrip("/") if parsed and parsed.path else os.getenv("POSTGRES_DB", "timetracker")

            pg_restore_cmd = [
                "pg_restore",
                "--clean",
                "--if-exists",
                "--no-owner",
                "-h",
                host,
                "-p",
                str(port),
                "-U",
                user,
                "-d",
                dbname,
                src_dump,
            ]
            env = os.environ.copy()
            if password:
                env["PGPASSWORD"] = str(password)
            try:
                _progress("Restoring PostgreSQL database", 20)
                subprocess.run(pg_restore_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
            except FileNotFoundError:
                return False, "pg_restore not found. Please install PostgreSQL client tools."
            except subprocess.CalledProcessError as e:
                stderr = e.stderr.decode("utf-8", errors="ignore") if e.stderr else ""
                return False, f"pg_restore failed: {stderr.strip() or e}"
            _progress("PostgreSQL database restored", 60)
        else:
            return False, "Unsupported or unknown database type for restore"

        # Restore uploads
        extracted_uploads = os.path.join(tmp_dir, "uploads")
        if os.path.isdir(extracted_uploads):
            target_uploads = os.path.join(app.root_path, "static", "uploads")
            os.makedirs(target_uploads, exist_ok=True)
            # Merge copy
            for root, _, files in os.walk(extracted_uploads):
                rel = os.path.relpath(root, extracted_uploads)
                target_dir = os.path.join(target_uploads, rel) if rel != "." else target_uploads
                os.makedirs(target_dir, exist_ok=True)
                for fn in files:
                    shutil.copy2(os.path.join(root, fn), os.path.join(target_dir, fn))
        _progress("Uploads restored", 80)

        # Run migrations to ensure compatibility with current code
        try:
            from flask_migrate import upgrade as alembic_upgrade

            with app.app_context():
                _progress("Running migrations", 90)
                alembic_upgrade()
        except Exception as e:
            # If migrations fail, report failure to caller for visibility
            return False, f"Restore completed but migration failed: {e}"

        # Dispose connections once more after restore/migrate to ensure clean state
        try:
            with app.app_context():
                db.session.remove()
                db.engine.dispose()
        except Exception as e:
            logger.debug("Session remove/dispose after restore failed: %s", e)

        _progress("Restore completed successfully", 100)
        return True, "Restore completed successfully"
    finally:
        try:
            shutil.rmtree(tmp_dir, ignore_errors=True)
        except Exception as e:
            logger.debug("Restore temp dir cleanup failed: %s", e)
