# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""Publish locally generated connector version artifacts to a GCS registry bucket.

This module uses `gcsfs.GCSFileSystem` to upload local artifacts
to the appropriate versioned path inside a GCS registry bucket.

The GCS destination path is:

    gs://<bucket>/[<prefix>/]metadata/airbyte/<connector>/<version>/
"""

from __future__ import annotations

import logging
import sys
from dataclasses import dataclass, field
from pathlib import Path

import gcsfs
import yaml

from airbyte_ops_mcp.registry._constants import (
    METADATA_FOLDER,
)
from airbyte_ops_mcp.registry._gcs_helpers import get_gcs_credentials_token
from airbyte_ops_mcp.registry.validate import (
    validate_metadata,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Result model
# ---------------------------------------------------------------------------


@dataclass
class PublishArtifactsResult:
    """Result of a version-artifacts publish operation."""

    connector_name: str
    version: str
    target: str
    gcs_destination: str
    files_uploaded: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    validation_errors: list[str] = field(default_factory=list)
    dry_run: bool = False

    @property
    def success(self) -> bool:
        return len(self.errors) == 0 and len(self.validation_errors) == 0

    @property
    def status(self) -> str:
        if self.dry_run:
            return "dry-run"
        if self.errors or self.validation_errors:
            return "completed-with-errors"
        return "success"


# ---------------------------------------------------------------------------
# Core publish logic
# ---------------------------------------------------------------------------


def _log_progress(msg: str, *args: object) -> None:
    """Log a progress message to both the logger and stderr."""
    logger.info(msg, *args)
    formatted = msg % args if args else msg
    print(formatted, file=sys.stderr, flush=True)


def publish_version_artifacts(
    connector_name: str,
    version: str,
    artifacts_dir: Path,
    bucket_name: str,
    prefix: str = "",
    dry_run: bool = False,
    with_validate: bool = True,
) -> PublishArtifactsResult:
    """Publish locally generated artifacts to a GCS registry bucket.

    Uses `gcsfs.GCSFileSystem` to upload the local *artifacts_dir* to the
    versioned path inside the target GCS bucket.

    The target GCS path is:
        `gs://<bucket>/[<prefix>/]metadata/airbyte/<connector>/<version>/`

    Args:
        connector_name: Connector name (e.g. `source-faker`).
        version: Version string (e.g. `6.2.38`).
        artifacts_dir: Local directory containing artifacts from `generate`.
        bucket_name: GCS bucket name to publish to.
        prefix: Optional path prefix within the bucket.
        dry_run: If *True*, report what would be uploaded without writing.
        with_validate: If *True* (default), validate metadata before uploading.
            Pass *False* (`--no-validate`) to skip.

    Returns:
        A `PublishArtifactsResult` describing what was published.
    """
    if not artifacts_dir.is_dir():
        raise FileNotFoundError(f"Artifacts directory not found: {artifacts_dir}")

    # Build the GCS destination path
    prefix_part = f"{prefix}/" if prefix else ""
    versioned_dest = (
        f"gcs://{bucket_name}/{prefix_part}"
        f"{METADATA_FOLDER}/airbyte/{connector_name}/{version}"
    )

    target_label = f"{bucket_name}/{prefix}" if prefix else bucket_name
    result = PublishArtifactsResult(
        connector_name=connector_name,
        version=version,
        target=target_label,
        gcs_destination=versioned_dest,
        dry_run=dry_run,
    )

    # --- Pre-publish validation ---
    if with_validate:
        metadata_file = artifacts_dir / "metadata.yaml"
        if metadata_file.is_file():
            raw_metadata = yaml.safe_load(metadata_file.read_text())
            metadata_data = (raw_metadata or {}).get("data", {})
            validation = validate_metadata(metadata_data=metadata_data)
            if not validation.passed:
                for err in validation.errors:
                    logger.error("Pre-publish validation error: %s", err)
                result.validation_errors = validation.errors
                return result
            logger.info(
                "Pre-publish validation passed (%d validators).",
                validation.validators_run,
            )
        else:
            logger.warning("No metadata.yaml in artifacts dir; skipping validation.")

    # Enumerate local files
    local_files = sorted(f for f in artifacts_dir.rglob("*") if f.is_file())
    if not local_files:
        result.errors.append(f"No files found in {artifacts_dir}.")
        return result

    _log_progress(
        "Publishing %d artifacts for %s@%s → %s",
        len(local_files),
        connector_name,
        version,
        versioned_dest,
    )

    if dry_run:
        for f in local_files:
            rel = f.relative_to(artifacts_dir)
            result.files_uploaded.append(str(rel))
            _log_progress("  [DRY RUN] would upload: %s", rel)
        return result

    # Authenticate
    token = get_gcs_credentials_token()
    fs = gcsfs.GCSFileSystem(token=token)

    # Strip gcs:// prefix for gcsfs path
    dest_path = versioned_dest.replace("gcs://", "")

    # Upload each file
    _log_progress("Uploading to: %s", versioned_dest)
    for f in local_files:
        rel = f.relative_to(artifacts_dir)
        remote_path = f"{dest_path}/{rel}"
        fs.put(str(f), remote_path)
        result.files_uploaded.append(str(rel))
        _log_progress("  Uploaded: %s", rel)

    # Delete remote files that don't exist locally (sync semantics)
    try:
        remote_files = fs.ls(dest_path, detail=False)
        local_rel_paths = {str(f.relative_to(artifacts_dir)) for f in local_files}
        for remote_file in remote_files:
            # Skip the directory entry itself if it appears in the listing
            if remote_file == dest_path:
                continue
            # Derive the remote relative path, matching upload semantics
            if remote_file.startswith(dest_path + "/"):
                remote_rel = remote_file[len(dest_path) + 1 :]
            else:
                remote_rel = remote_file.split("/")[-1]
            if remote_rel not in local_rel_paths:
                fs.rm(remote_file)
                _log_progress("  Deleted stale remote file: %s", remote_rel)
    except FileNotFoundError:
        pass  # Destination doesn't exist yet, nothing to clean

    _log_progress("Uploaded %d files to %s", len(local_files), versioned_dest)

    return result
