import json
import re
import shlex


class BackupManager:
    """Create/list downloadable protocol backups on the remote server.

    Backups are intentionally allowlisted per protocol and include only files
    needed to recreate protocol state: host config directories, matching
    container config directories when present, and docker inspect metadata.
    """

    BACKUP_ROOT = '/opt/amnezia/backups'

    def __init__(self, ssh_manager):
        self.ssh = ssh_manager

    @staticmethod
    def proto_base(protocol):
        return str(protocol or '').split('__', 1)[0]

    @staticmethod
    def proto_instance(protocol):
        match = re.search(r'__(\d+)$', str(protocol or ''))
        return int(match.group(1)) if match else 1

    @staticmethod
    def safe_protocol(protocol):
        safe = re.sub(r'[^a-zA-Z0-9_.-]+', '_', str(protocol or '').replace('__', '-'))
        return safe.strip('._-') or 'protocol'

    @staticmethod
    def safe_filename(filename):
        name = str(filename or '')
        if not re.fullmatch(r'[A-Za-z0-9_.-]+\.tar\.gz', name):
            return None
        return name

    def _paths_for(self, protocol, container_name):
        base = self.proto_base(protocol)
        idx = self.proto_instance(protocol)
        paths = {
            'host': [],
            'container': [],
        }

        def inst_path(path, suffix_fmt='-{idx}'):
            return path if idx <= 1 else f"{path}{suffix_fmt.format(idx=idx)}"

        if base in ('awg', 'awg2', 'awg_legacy'):
            paths['host'] = ['/opt/amnezia/awg', f'/opt/amnezia/{container_name}']
            paths['container'] = ['/opt/amnezia/awg', '/opt/amnezia/start.sh']
        elif base == 'wireguard':
            paths['host'] = ['/opt/amnezia/wireguard', f'/opt/amnezia/{container_name}']
            paths['container'] = ['/opt/amnezia/wireguard', '/opt/amnezia/start.sh']
        elif base == 'xray':
            config_dir = inst_path('/opt/amnezia/xray')
            paths['host'] = [config_dir, f'/opt/amnezia/{container_name}']
            paths['container'] = [config_dir]
        elif base == 'telemt':
            remote_dir = inst_path('/opt/amnezia/telemt')
            paths['host'] = [remote_dir]
            paths['container'] = [remote_dir]
        elif base == 'hysteria':
            remote_dir = inst_path('/opt/amnezia/hysteria')
            paths['host'] = [remote_dir]
            paths['container'] = ['/etc/hysteria']
        elif base == 'naiveproxy':
            remote_dir = inst_path('/opt/amnezia/naiveproxy')
            paths['host'] = [remote_dir]
            paths['container'] = ['/etc/caddy']
        elif base == 'mieru':
            remote_dir = inst_path('/opt/amnezia/mieru')
            paths['host'] = [remote_dir]
        elif base == 'dns':
            paths['host'] = ['/opt/amnezia/dns']
            paths['container'] = ['/opt/amnezia/dns']
        elif base == 'adguard':
            paths['host'] = ['/opt/amnezia/adguard']
            paths['container'] = ['/opt/adguardhome/conf', '/opt/adguardhome/work']
        elif base == 'socks5':
            config_dir = inst_path('/opt/amnezia/socks5proxy')
            paths['host'] = [config_dir]
            paths['container'] = ['/etc/3proxy']
        elif base == 'nginx':
            paths['host'] = ['/opt/amnezia/nginx']
            paths['container'] = ['/etc/nginx/conf.d', '/usr/share/nginx/html']
        else:
            paths['host'] = [f'/opt/amnezia/{base}']
            paths['container'] = [f'/opt/amnezia/{base}']

        return paths

    def list_backups(self, protocol):
        safe_proto = self.safe_protocol(protocol)
        backup_dir = f'{self.BACKUP_ROOT}/{safe_proto}'
        cmd = (
            f"mkdir -p {shlex.quote(backup_dir)} && "
            f"find {shlex.quote(backup_dir)} -maxdepth 1 -type f -name '*.tar.gz' "
            "-printf '%f|%s|%T@\\n' 2>/dev/null | sort -t '|' -k3,3nr"
        )
        out, err, code = self.ssh.run_sudo_command(cmd)
        if code != 0:
            return {'status': 'error', 'message': err or out or 'Failed to list backups'}
        backups = []
        for line in (out or '').splitlines():
            parts = line.split('|')
            if len(parts) != 3:
                continue
            name, size, mtime = parts
            backups.append({
                'name': name,
                'size': int(float(size or 0)),
                'mtime': float(mtime or 0),
            })
        return {'status': 'success', 'protocol': protocol, 'backups': backups}

    def create_backup(self, protocol, container_name):
        safe_proto = self.safe_protocol(protocol)
        backup_dir = f'{self.BACKUP_ROOT}/{safe_proto}'
        paths = self._paths_for(protocol, container_name)
        host_paths = ' '.join(shlex.quote(p) for p in paths['host'])
        container_paths = ' '.join(shlex.quote(p) for p in paths['container'])
        protocol_q = shlex.quote(str(protocol))
        safe_proto_q = shlex.quote(safe_proto)
        container_q = shlex.quote(str(container_name or ''))
        backup_dir_q = shlex.quote(backup_dir)

        script = f"""
set -eu
umask 077
protocol={protocol_q}
safe_proto={safe_proto_q}
container={container_q}
backup_dir={backup_dir_q}
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
work_dir=$(mktemp -d /tmp/amnezia-backup-${{safe_proto}}.XXXXXX)
cleanup() {{ rm -rf "$work_dir"; }}
trap cleanup EXIT
mkdir -p "$backup_dir" "$work_dir/host" "$work_dir/container" "$work_dir/docker"
cat > "$work_dir/backup-info.json" <<EOF
{{
  "protocol": "$protocol",
  "container": "$container",
  "created_at_utc": "$timestamp",
  "backup_format": "amnezia-web-panel-protocol-v1"
}}
EOF
copy_host_path() {{
  src="$1"
  if [ -e "$src" ]; then
    mkdir -p "$work_dir/host$(dirname "$src")"
    cp -a "$src" "$work_dir/host$src"
  fi
}}
copy_container_path() {{
  src="$1"
  if [ -n "$container" ] && docker inspect "$container" >/dev/null 2>&1; then
    mkdir -p "$work_dir/container$(dirname "$src")"
    docker cp "$container:$src" "$work_dir/container$src" >/dev/null 2>&1 || true
  fi
}}
for p in {host_paths}; do copy_host_path "$p"; done
for p in {container_paths}; do copy_container_path "$p"; done
if [ -n "$container" ] && docker inspect "$container" >/dev/null 2>&1; then
  docker inspect "$container" > "$work_dir/docker/inspect.json" 2>/dev/null || true
  docker logs --tail 300 "$container" > "$work_dir/docker/logs-tail.txt" 2>&1 || true
fi
archive="$backup_dir/${{safe_proto}}-${{timestamp}}.tar.gz"
tar -C "$work_dir" -czf "$archive" .
chmod 0644 "$archive"
printf '%s\n' "$archive"
""".strip()

        out, err, code = self.ssh.run_sudo_command(script)
        if code != 0:
            return {'status': 'error', 'message': err or out or 'Failed to create backup'}
        path = (out or '').strip().splitlines()[-1] if (out or '').strip() else ''
        name = path.rsplit('/', 1)[-1] if path else ''
        return {'status': 'success', 'protocol': protocol, 'backup': {'name': name, 'path': path}}

    def restore_backup(self, protocol, container_name, filename):
        """Restore protocol files from a previously created backup archive."""
        safe_name = self.safe_filename(filename)
        if not safe_name:
            return {'status': 'error', 'message': 'Invalid backup filename'}

        safe_proto = self.safe_protocol(protocol)
        remote_archive = f'{self.BACKUP_ROOT}/{safe_proto}/{safe_name}'
        paths = self._paths_for(protocol, container_name)
        container_paths = ' '.join(shlex.quote(p) for p in paths['container'])
        host_paths = ' '.join(shlex.quote(p) for p in paths['host'])
        archive_q = shlex.quote(remote_archive)
        container_q = shlex.quote(str(container_name or ''))
        protocol_q = shlex.quote(str(protocol))

        script = f"""
set -eu
umask 077
archive={archive_q}
container={container_q}
protocol={protocol_q}
if [ ! -f "$archive" ]; then
  echo "Backup archive not found" >&2
  exit 1
fi
work_dir=$(mktemp -d /tmp/amnezia-restore-XXXXXX)
cleanup() {{ rm -rf "$work_dir"; }}
trap cleanup EXIT
tar -xzf "$archive" -C "$work_dir"
if [ ! -f "$work_dir/backup-info.json" ]; then
  echo "Invalid backup archive (missing backup-info.json)" >&2
  exit 1
fi
restore_host_path() {{
  src="$1"
  staged="$work_dir/host$src"
  if [ -e "$staged" ]; then
    mkdir -p "$(dirname "$src")"
    rm -rf "$src"
    cp -a "$staged" "$src"
  fi
}}
restore_container_path() {{
  src="$1"
  staged="$work_dir/container$src"
  if [ -n "$container" ] && [ -e "$staged" ] && docker inspect "$container" >/dev/null 2>&1; then
    if [ -d "$staged" ]; then
      docker exec "$container" mkdir -p "$src" >/dev/null 2>&1 || true
      docker cp "$staged/." "$container:$src/" >/dev/null 2>&1 || true
    else
      parent=$(dirname "$src")
      docker exec "$container" mkdir -p "$parent" >/dev/null 2>&1 || true
      docker cp "$staged" "$container:$src" >/dev/null 2>&1 || true
    fi
  fi
}}
for p in {host_paths}; do restore_host_path "$p"; done
for p in {container_paths}; do restore_container_path "$p"; done
if [ -n "$container" ] && docker inspect "$container" >/dev/null 2>&1; then
  docker restart "$container" >/dev/null 2>&1 || true
  # Entrypoint usually brings the interface up; give start.sh a short bounded window
  if docker exec "$container" test -x /opt/amnezia/start.sh >/dev/null 2>&1; then
    timeout 25 docker exec "$container" /opt/amnezia/start.sh >/dev/null 2>&1 || true
  fi
fi
printf 'restored:%s\\n' "$protocol"
""".strip()

        out, err, code = self.ssh.run_sudo_command(script, timeout=120)
        if code != 0:
            return {'status': 'error', 'message': err or out or 'Failed to restore backup'}
        return {'status': 'success', 'protocol': protocol, 'filename': safe_name}
