"""SFTP Backup Service - handles backup upload/download/versioning."""
import io
import json
import os
import sqlite3
import stat
import tempfile
import zipfile
from datetime import datetime, timezone
from pathlib import Path

import paramiko


def get_sftp_connection(target, app_config=None):
    """Create SFTP connection from a BackupTarget."""
    from app.services.crypto_service import decrypt_field

    transport = paramiko.Transport((target.host, target.port))

    if target.private_key_encrypted:
        key_pem = decrypt_field(target.private_key_encrypted, 'backup-key')
        pkey = paramiko.RSAKey.from_private_key(io.StringIO(key_pem))
        transport.connect(username=target.username, pkey=pkey)
    elif target.password_encrypted:
        password = decrypt_field(target.password_encrypted, 'backup-key')
        transport.connect(username=target.username, password=password)
    else:
        raise ValueError('Weder Passwort noch Private Key konfiguriert')

    return paramiko.SFTPClient.from_transport(transport), transport


def ensure_remote_dir(sftp, path):
    """Recursively create remote directories."""
    dirs = []
    while path and path != '/' and path != '.':
        try:
            sftp.stat(path)
            break
        except FileNotFoundError:
            dirs.insert(0, path)
            path = os.path.dirname(path)
    for d in dirs:
        try:
            sftp.mkdir(d)
        except IOError:
            pass


def create_backup_zip(db_path, upload_path):
    """Create a backup ZIP and return the temp file path."""
    from app.models.user import User
    from app.models.file import File

    tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
    tmp_path = tmp.name
    tmp.close()

    metadata = {
        'version': '1.0',
        'created_at': datetime.now(timezone.utc).isoformat(),
        'description': 'Mini-Cloud SFTP Backup',
    }

    with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zf:
        zf.writestr('metadata.json', json.dumps(metadata, indent=2))

        # DB backup
        db_backup_path = tmp_path + '.db'
        try:
            source = sqlite3.connect(db_path)
            dest = sqlite3.connect(db_backup_path)
            source.backup(dest)
            source.close()
            dest.close()
            zf.write(db_backup_path, 'database.sqlite3')
        finally:
            if os.path.exists(db_backup_path):
                os.unlink(db_backup_path)

        # Files
        upload_dir = Path(upload_path)
        if upload_dir.exists():
            for fp in upload_dir.rglob('*'):
                if fp.is_file():
                    zf.write(str(fp), 'files/' + str(fp.relative_to(upload_dir)))

    return tmp_path


def upload_backup_to_sftp(target, zip_path, app):
    """Upload backup ZIP to SFTP target in chunks. Returns version name."""
    timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
    version_name = f'backup_{timestamp}.zip'

    sftp, transport = get_sftp_connection(target)
    try:
        remote_dir = target.remote_path.rstrip('/')
        ensure_remote_dir(sftp, remote_dir)

        remote_file = f'{remote_dir}/{version_name}'

        # Upload in chunks (16MB)
        file_size = os.path.getsize(zip_path)
        chunk_size = 16 * 1024 * 1024

        with open(zip_path, 'rb') as local_file:
            with sftp.open(remote_file, 'wb') as remote:
                remote.set_pipelined(True)
                while True:
                    data = local_file.read(chunk_size)
                    if not data:
                        break
                    remote.write(data)

        # Write a latest marker
        sftp.open(f'{remote_dir}/latest.txt', 'w').write(version_name)

        # Cleanup old versions
        _cleanup_old_versions(sftp, remote_dir, target.max_versions)

        return version_name
    finally:
        sftp.close()
        transport.close()


def _cleanup_old_versions(sftp, remote_dir, max_versions):
    """Remove old backup versions exceeding max_versions."""
    try:
        entries = sftp.listdir_attr(remote_dir)
        backups = sorted(
            [e for e in entries if e.filename.startswith('backup_') and e.filename.endswith('.zip')],
            key=lambda e: e.st_mtime or 0,
            reverse=True,
        )
        for old in backups[max_versions:]:
            try:
                sftp.remove(f'{remote_dir}/{old.filename}')
            except Exception:
                pass
    except Exception:
        pass


def list_sftp_versions(target):
    """List available backup versions on SFTP target."""
    sftp, transport = get_sftp_connection(target)
    try:
        remote_dir = target.remote_path.rstrip('/')
        try:
            entries = sftp.listdir_attr(remote_dir)
        except FileNotFoundError:
            return []

        versions = []
        for e in entries:
            if e.filename.startswith('backup_') and e.filename.endswith('.zip'):
                versions.append({
                    'name': e.filename,
                    'size': e.st_size,
                    'modified': datetime.fromtimestamp(e.st_mtime, tz=timezone.utc).isoformat()
                    if e.st_mtime else None,
                })

        versions.sort(key=lambda v: v['name'], reverse=True)
        return versions
    finally:
        sftp.close()
        transport.close()


def download_version_from_sftp(target, version_name):
    """Download a specific backup version from SFTP. Returns temp file path."""
    sftp, transport = get_sftp_connection(target)
    try:
        remote_dir = target.remote_path.rstrip('/')
        remote_file = f'{remote_dir}/{version_name}'

        tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
        tmp_path = tmp.name
        tmp.close()

        # Download in chunks
        with sftp.open(remote_file, 'rb') as remote:
            with open(tmp_path, 'wb') as local:
                while True:
                    data = remote.read(16 * 1024 * 1024)
                    if not data:
                        break
                    local.write(data)

        return tmp_path
    finally:
        sftp.close()
        transport.close()


def test_sftp_connection(target):
    """Test SFTP connection. Returns True on success, raises on error."""
    sftp, transport = get_sftp_connection(target)
    try:
        remote_dir = target.remote_path.rstrip('/')
        ensure_remote_dir(sftp, remote_dir)
        # Try to write a test file
        test_file = f'{remote_dir}/.minicloud_test'
        sftp.open(test_file, 'w').write('ok')
        sftp.remove(test_file)
        return True
    finally:
        sftp.close()
        transport.close()
