use crate::{
    error::{ErrorData, Result},
    traits::{
        ArtifactRegistry, ArtifactRegistryCredentials, ArtifactRegistryPermissions, Binding,
        CrossAccountAccess, CrossAccountPermissions, RepositoryResponse,
    },
};
use alien_core::bindings::ArtifactRegistryBinding;
use alien_error::{AlienError, Context, ContextError, IntoAlienError, IntoAlienErrorDirect};
use async_trait::async_trait;
use oci_client::{
    client::{Client as OciClient, ClientConfig as OciClientConfig, ClientProtocol},
    errors::OciDistributionError,
    secrets::RegistryAuth,
    Reference,
};
use tracing::{debug, info};

/// Local artifact registry implementation that connects to an external container registry.
///
/// This is a **client** that connects to a local container registry server
/// (e.g., started by LocalArtifactRegistryManager in alien-local).
///
/// Unlike cloud providers that have explicit repository creation APIs, Docker registries
/// implicitly create repositories on first push. To provide a consistent interface,
/// this implementation pushes a minimal empty manifest when `create_repository()` is called,
/// ensuring the repository exists and can be queried immediately afterward.
#[derive(Debug)]
pub struct LocalArtifactRegistry {
    binding_name: String,
    registry_endpoint: String,
}

impl LocalArtifactRegistry {
    /// Creates a new local artifact registry instance from binding parameters.
    ///
    /// # Arguments
    /// * `binding_name` - The name of this binding
    /// * `binding` - The binding configuration containing registry settings
    pub async fn new(
        binding_name: String,
        binding: alien_core::bindings::ArtifactRegistryBinding,
    ) -> Result<Self> {
        // Extract fields from Local variant
        let config = match binding {
            ArtifactRegistryBinding::Local(config) => config,
            _ => {
                return Err(AlienError::new(ErrorData::BindingConfigInvalid {
                    binding_name,
                    reason: "Expected Local artifact registry binding variant".to_string(),
                }));
            }
        };

        let registry_endpoint = config
            .registry_url
            .into_value(&binding_name, "registry_url")
            .context(ErrorData::BindingConfigInvalid {
                binding_name: binding_name.clone(),
                reason: "Failed to extract registry_url from binding".to_string(),
            })?;

        // Validate the registry endpoint format
        if registry_endpoint.is_empty() {
            return Err(AlienError::new(ErrorData::BindingConfigInvalid {
                binding_name: binding_name.clone(),
                reason: "Registry endpoint cannot be empty".to_string(),
            }));
        }

        info!(
            binding_name = %binding_name,
            endpoint = %registry_endpoint,
            "Local artifact registry client configured"
        );

        Ok(Self {
            binding_name,
            registry_endpoint,
        })
    }

    /// Gets the registry endpoint for this local registry
    pub fn registry_endpoint(&self) -> &str {
        &self.registry_endpoint
    }

    /// Creates an OCI client for communicating with the local registry
    fn create_oci_client(&self) -> OciClient {
        OciClient::new(OciClientConfig {
            protocol: ClientProtocol::Http,
            ..Default::default()
        })
    }

    /// Creates an OCI Reference for a repository in this registry
    fn create_reference(&self, repo_id: &str) -> Result<Reference> {
        // registry_endpoint is like "localhost:5000"
        // The container-registry crate requires a two-level path: /v2/:repository/:image/...
        // We use the binding name as :repository and repo_id as :image to match the conceptual
        // model: "artifacts registry contains alien-prj_xxx repository"
        // This also enables namespace separation for multiple ArtifactRegistry resources.
        let ref_string = format!(
            "{}/{}/{}:latest",
            self.registry_endpoint, self.binding_name, repo_id
        );
        Reference::try_from(ref_string.as_str())
            .into_alien_error()
            .context(ErrorData::Other {
                message: format!("Invalid repository reference: {}", ref_string),
            })
    }
}

impl Binding for LocalArtifactRegistry {}

#[async_trait]
impl ArtifactRegistry for LocalArtifactRegistry {
    fn registry_endpoint(&self) -> String {
        let host = &self.registry_endpoint;
        if host.starts_with("http://") || host.starts_with("https://") {
            host.clone()
        } else {
            format!("http://{}", host)
        }
    }

    fn upstream_repository_prefix(&self) -> String {
        // The embedded local registry accepts two-segment repo paths (e.g.,
        // "namespace/repo"). We use "artifacts/default" as the canonical prefix
        // — this matches what the CLI hardcodes in dev mode and what the proxy
        // routing table uses to route pushes to this local registry.
        "artifacts/default".to_string()
    }

    async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse> {
        info!(
            binding_name = %self.binding_name,
            repo_name = %repo_name,
            "Creating local Docker repository"
        );

        // For Docker registries, repositories are created implicitly on first manifest push
        // We push a minimal empty OCI Image Manifest to make the repository exist
        // This ensures consistent behavior with cloud providers where create_repository
        // makes the repository immediately queryable

        let client = self.create_oci_client();
        let reference = self.create_reference(repo_name)?;

        // Create a minimal OCI Image Manifest with inline config and no layers
        use oci_client::manifest::{OciDescriptor, OciImageManifest, OciManifest};

        // Create minimal empty config
        let config_json = serde_json::json!({
            "architecture": "amd64",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": []
            },
            "config": {}
        });

        let config_bytes = serde_json::to_vec(&config_json)
            .into_alien_error()
            .context(ErrorData::Other {
                message: "Failed to serialize config".to_string(),
            })?;

        // Calculate SHA256 digest for config
        use sha2::{Digest as Sha2Digest, Sha256};
        let config_digest = format!("sha256:{:x}", Sha256::digest(&config_bytes));

        // Create config descriptor
        let config_descriptor = OciDescriptor {
            media_type: "application/vnd.oci.image.config.v1+json".to_string(),
            size: config_bytes.len() as i64,
            digest: config_digest.clone(),
            urls: None,
            annotations: None,
        };

        // Create minimal manifest with just the config (no layers)
        let manifest = OciImageManifest {
            schema_version: 2,
            media_type: Some("application/vnd.oci.image.manifest.v1+json".to_string()),
            config: config_descriptor,
            layers: vec![], // Empty - no layers
            annotations: Some({
                let mut map = std::collections::BTreeMap::new();
                map.insert(
                    "dev.alien.marker".to_string(),
                    "empty-repository-created-by-alien".to_string(),
                );
                map
            }),
            subject: None,
            artifact_type: None,
        };

        // Push the config blob first (OCI spec requires all referenced blobs to exist before
        // pushing a manifest), then push the manifest to create the repository.
        let auth = RegistryAuth::Anonymous;
        client
            .store_auth_if_needed(&self.registry_endpoint, &auth)
            .await;

        client
            .push_blob(&reference, &config_bytes, &config_digest)
            .await
            .into_alien_error()
            .context(ErrorData::Other {
                message: format!("Failed to push config blob for repository '{}'", repo_name),
            })?;

        client
            .push_manifest(&reference, &OciManifest::Image(manifest))
            .await
            .into_alien_error()
            .context(ErrorData::Other {
                message: format!(
                    "Failed to push marker manifest for repository '{}'",
                    repo_name
                ),
            })?;

        // Repository URI uses binding name as first component for namespace separation.
        // Format: registry/binding-name/repository (e.g., localhost:5000/artifacts/alien-prj_xxx)
        // This satisfies container-registry's two-level requirement and provides semantic clarity.
        let repository_uri = format!(
            "{}/{}/{}",
            self.registry_endpoint, self.binding_name, repo_name
        );

        info!(
            binding_name = %self.binding_name,
            repo_name = %repo_name,
            uri = %repository_uri,
            "Local Docker repository created successfully"
        );

        Ok(RepositoryResponse {
            name: repo_name.to_string(),
            uri: Some(repository_uri),
            created_at: None,
        })
    }

    async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse> {
        debug!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Checking local repository existence via OCI API"
        );

        // Use oci-client to check if repository exists by trying to fetch a manifest
        let client = self.create_oci_client();
        let reference = self.create_reference(repo_id)?;

        // Store auth credentials for this registry
        let auth = RegistryAuth::Anonymous;
        client
            .store_auth_if_needed(&self.registry_endpoint, &auth)
            .await;

        // Try to pull the manifest we created (or any manifest with :latest tag)
        // This hits /v2/<repository>/<image>/manifests/<reference> endpoint
        match client.pull_manifest(&reference, &auth).await {
            Ok(_) => {
                // Repository exists and has at least one manifest
                // URI format matches create_repository: registry/binding-name/repository
                let repository_uri = format!(
                    "{}/{}/{}",
                    self.registry_endpoint, self.binding_name, repo_id
                );

                debug!(
                    binding_name = %self.binding_name,
                    repo_id = %repo_id,
                    repo_uri = %repository_uri,
                    "Local repository exists"
                );

                Ok(RepositoryResponse {
                    name: repo_id.to_string(),
                    uri: Some(repository_uri),
                    created_at: None,
                })
            }
            Err(OciDistributionError::ServerError { code: 404, .. }) => {
                // Repository or manifest doesn't exist (404 from registry)
                debug!(
                    binding_name = %self.binding_name,
                    repo_id = %repo_id,
                    "Local repository not found (404)"
                );

                Err(AlienError::new(ErrorData::ResourceNotFound {
                    resource_id: repo_id.to_string(),
                }))
            }
            Err(OciDistributionError::ImageManifestNotFoundError(_)) => {
                // Manifest doesn't exist - treat as repository not found
                debug!(
                    binding_name = %self.binding_name,
                    repo_id = %repo_id,
                    "Local repository not found (manifest not found)"
                );

                Err(AlienError::new(ErrorData::ResourceNotFound {
                    resource_id: repo_id.to_string(),
                }))
            }
            Err(OciDistributionError::RegistryError { envelope, .. })
                if envelope.errors.iter().any(|e| {
                    matches!(
                        e.code,
                        oci_client::errors::OciErrorCode::BlobUnknown
                            | oci_client::errors::OciErrorCode::ManifestUnknown
                            | oci_client::errors::OciErrorCode::NameUnknown
                    )
                }) =>
            {
                // Blob/manifest/repository doesn't exist - expected "not found" case
                debug!(
                    binding_name = %self.binding_name,
                    repo_id = %repo_id,
                    "Local repository not found (OCI error: blob/manifest/name unknown)"
                );

                Err(AlienError::new(ErrorData::ResourceNotFound {
                    resource_id: repo_id.to_string(),
                }))
            }
            Err(e) => {
                // Actual unexpected errors (connection issues, auth failures, etc.)
                // Fail fast - don't silently treat these as "not found"
                Err(e.into_alien_error().context(ErrorData::Other {
                    message: "Failed to check repository existence".to_string(),
                }))
            }
        }
    }

    async fn add_cross_account_access(
        &self,
        repo_id: &str,
        _access: CrossAccountAccess,
    ) -> Result<()> {
        info!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Local artifact registry cross-account access not supported"
        );

        Err(AlienError::new(ErrorData::OperationNotSupported {
            operation: "add_cross_account_access".to_string(),
            reason: "Local artifact registry does not support cross-account access".to_string(),
        }))
    }

    async fn remove_cross_account_access(
        &self,
        repo_id: &str,
        _access: CrossAccountAccess,
    ) -> Result<()> {
        info!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Local artifact registry cross-account access not supported"
        );

        Err(AlienError::new(ErrorData::OperationNotSupported {
            operation: "remove_cross_account_access".to_string(),
            reason: "Local artifact registry does not support cross-account access".to_string(),
        }))
    }

    async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions> {
        info!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Local artifact registry cross-account access not supported"
        );

        Err(AlienError::new(ErrorData::OperationNotSupported {
            operation: "get_cross_account_access".to_string(),
            reason: "Local artifact registry does not support cross-account access".to_string(),
        }))
    }

    async fn generate_credentials(
        &self,
        repo_id: &str,
        permissions: ArtifactRegistryPermissions,
        ttl_seconds: Option<u32>,
    ) -> Result<ArtifactRegistryCredentials> {
        info!(
            repo_id = %repo_id,
            permissions = ?permissions,
            ttl_seconds = ?ttl_seconds,
            "Generating local artifact registry credentials"
        );

        // Local registry runs on localhost without auth.
        // Return empty credentials — callers should use anonymous access.
        Ok(ArtifactRegistryCredentials {
            username: String::new(),
            password: String::new(),
            expires_at: None,
        })
    }

    async fn get_manifest(&self, repo_name: &str, reference: &str) -> Result<(Vec<u8>, String)> {
        // Fetch raw manifest bytes via HTTP to preserve exact byte content.
        // Re-serializing through oci-client would change whitespace and break
        // digest verification by OCI clients.
        let url = format!(
            "http://{}/v2/{}/manifests/{}",
            self.registry_endpoint, repo_name, reference
        );

        let http_client = reqwest::Client::new();
        let resp = http_client
            .get(&url)
            .header("accept", "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json")
            .send()
            .await
            .into_alien_error()
            .context(ErrorData::Other {
                message: format!("Failed to fetch manifest for {}:{}", repo_name, reference),
            })?;

        if !resp.status().is_success() {
            return Err(AlienError::new(ErrorData::Other {
                message: format!(
                    "Upstream registry returned {} for manifest {}:{}",
                    resp.status(),
                    repo_name,
                    reference
                ),
            }));
        }

        let content_type = resp
            .headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("application/vnd.oci.image.manifest.v1+json")
            .to_string();

        let body = resp
            .bytes()
            .await
            .into_alien_error()
            .context(ErrorData::Other {
                message: "Failed to read manifest body".to_string(),
            })?;

        Ok((body.to_vec(), content_type))
    }

    async fn head_manifest(
        &self,
        repo_name: &str,
        reference: &str,
    ) -> Result<Option<(String, String)>> {
        // HEAD request via HTTP to get the digest without pulling the full manifest.
        let url = format!(
            "http://{}/v2/{}/manifests/{}",
            self.registry_endpoint, repo_name, reference
        );

        let http_client = reqwest::Client::new();
        match http_client
            .head(&url)
            .header("accept", "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json")
            .send()
            .await
        {
            Ok(resp) if resp.status().is_success() => {
                let content_type = resp
                    .headers()
                    .get("content-type")
                    .and_then(|v| v.to_str().ok())
                    .unwrap_or("application/vnd.oci.image.manifest.v1+json")
                    .to_string();
                let digest = resp
                    .headers()
                    .get("docker-content-digest")
                    .and_then(|v| v.to_str().ok())
                    .unwrap_or("")
                    .to_string();
                if digest.is_empty() {
                    Ok(None)
                } else {
                    Ok(Some((content_type, digest)))
                }
            }
            _ => Ok(None),
        }
    }

    async fn head_blob(&self, repo_name: &str, digest: &str) -> Result<Option<u64>> {
        // HEAD request to the OCI blob endpoint
        let url = format!(
            "http://{}/v2/{}/blobs/{}",
            self.registry_endpoint, repo_name, digest
        );

        let http_client = reqwest::Client::new();
        match http_client.head(&url).send().await {
            Ok(resp) if resp.status().is_success() => {
                let content_length = resp
                    .headers()
                    .get("content-length")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|v| v.parse().ok())
                    .unwrap_or(0);
                Ok(Some(content_length))
            }
            _ => Ok(None),
        }
    }

    async fn delete_repository(&self, repo_id: &str) -> Result<()> {
        info!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Deleting local repository (stateless - no-op)"
        );

        // For local registries, deletion is a no-op since we don't track state.
        // The actual registry server handles storage.
        info!(
            binding_name = %self.binding_name,
            repo_id = %repo_id,
            "Local repository deletion acknowledged (no-op for stateless client)"
        );

        Ok(())
    }
}
