use forage_core::auth::{
    AuthError, AuthTokens, CreatedToken, ForestAuth, PersonalAccessToken, User, UserEmail,
};
use forage_core::platform::{
    Artifact, ArtifactContext, ArtifactDestination, ArtifactSource, Destination, ForestPlatform,
    Organisation, OrgMember, PlatformError,
};
use forage_grpc::organisation_service_client::OrganisationServiceClient;
use forage_grpc::release_service_client::ReleaseServiceClient;
use forage_grpc::users_service_client::UsersServiceClient;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;

fn bearer_request<T>(access_token: &str, msg: T) -> Result<Request<T>, String> {
    let mut req = Request::new(msg);
    let bearer: MetadataValue<_> = format!("Bearer {access_token}")
        .parse()
        .map_err(|_| "invalid token format".to_string())?;
    req.metadata_mut().insert("authorization", bearer);
    Ok(req)
}

/// Real gRPC client to forest-server's UsersService.
#[derive(Clone)]
pub struct GrpcForestClient {
    channel: Channel,
}

impl GrpcForestClient {
    /// Create a client that connects lazily (for when server may not be available at startup).
    pub fn connect_lazy(endpoint: &str) -> anyhow::Result<Self> {
        let channel = Channel::from_shared(endpoint.to_string())?.connect_lazy();
        Ok(Self { channel })
    }

    fn client(&self) -> UsersServiceClient<Channel> {
        UsersServiceClient::new(self.channel.clone())
    }

    fn org_client(&self) -> OrganisationServiceClient<Channel> {
        OrganisationServiceClient::new(self.channel.clone())
    }

    fn release_client(&self) -> ReleaseServiceClient<Channel> {
        ReleaseServiceClient::new(self.channel.clone())
    }

    fn authed_request<T>(access_token: &str, msg: T) -> Result<Request<T>, AuthError> {
        bearer_request(access_token, msg).map_err(AuthError::Other)
    }
}

fn map_status(status: tonic::Status) -> AuthError {
    match status.code() {
        tonic::Code::Unauthenticated => AuthError::InvalidCredentials,
        tonic::Code::AlreadyExists => AuthError::AlreadyExists(status.message().into()),
        tonic::Code::PermissionDenied => AuthError::NotAuthenticated,
        tonic::Code::Unavailable => AuthError::Unavailable(status.message().into()),
        _ => AuthError::Other(status.message().into()),
    }
}

fn convert_user(u: forage_grpc::User) -> User {
    User {
        user_id: u.user_id,
        username: u.username,
        emails: u
            .emails
            .into_iter()
            .map(|e| UserEmail {
                email: e.email,
                verified: e.verified,
            })
            .collect(),
    }
}

fn convert_token(t: forage_grpc::PersonalAccessToken) -> PersonalAccessToken {
    PersonalAccessToken {
        token_id: t.token_id,
        name: t.name,
        scopes: t.scopes,
        created_at: t.created_at.map(|ts| ts.to_string()),
        last_used: t.last_used.map(|ts| ts.to_string()),
        expires_at: t.expires_at.map(|ts| ts.to_string()),
    }
}

#[async_trait::async_trait]
impl ForestAuth for GrpcForestClient {
    async fn register(
        &self,
        username: &str,
        email: &str,
        password: &str,
    ) -> Result<AuthTokens, AuthError> {
        let resp = self
            .client()
            .register(forage_grpc::RegisterRequest {
                username: username.into(),
                email: email.into(),
                password: password.into(),
            })
            .await
            .map_err(map_status)?
            .into_inner();

        let tokens = resp.tokens.ok_or(AuthError::Other("no tokens in response".into()))?;
        Ok(AuthTokens {
            access_token: tokens.access_token,
            refresh_token: tokens.refresh_token,
            expires_in_seconds: tokens.expires_in_seconds,
        })
    }

    async fn login(&self, identifier: &str, password: &str) -> Result<AuthTokens, AuthError> {
        let login_identifier = if identifier.contains('@') {
            forage_grpc::login_request::Identifier::Email(identifier.into())
        } else {
            forage_grpc::login_request::Identifier::Username(identifier.into())
        };

        let resp = self
            .client()
            .login(forage_grpc::LoginRequest {
                identifier: Some(login_identifier),
                password: password.into(),
            })
            .await
            .map_err(map_status)?
            .into_inner();

        let tokens = resp.tokens.ok_or(AuthError::Other("no tokens in response".into()))?;
        Ok(AuthTokens {
            access_token: tokens.access_token,
            refresh_token: tokens.refresh_token,
            expires_in_seconds: tokens.expires_in_seconds,
        })
    }

    async fn refresh_token(&self, refresh_token: &str) -> Result<AuthTokens, AuthError> {
        let resp = self
            .client()
            .refresh_token(forage_grpc::RefreshTokenRequest {
                refresh_token: refresh_token.into(),
            })
            .await
            .map_err(map_status)?
            .into_inner();

        let tokens = resp
            .tokens
            .ok_or(AuthError::Other("no tokens in response".into()))?;
        Ok(AuthTokens {
            access_token: tokens.access_token,
            refresh_token: tokens.refresh_token,
            expires_in_seconds: tokens.expires_in_seconds,
        })
    }

    async fn logout(&self, refresh_token: &str) -> Result<(), AuthError> {
        self.client()
            .logout(forage_grpc::LogoutRequest {
                refresh_token: refresh_token.into(),
            })
            .await
            .map_err(map_status)?;
        Ok(())
    }

    async fn get_user(&self, access_token: &str) -> Result<User, AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::TokenInfoRequest {},
        )?;

        let info = self
            .client()
            .token_info(req)
            .await
            .map_err(map_status)?
            .into_inner();

        let req = Self::authed_request(
            access_token,
            forage_grpc::GetUserRequest {
                identifier: Some(forage_grpc::get_user_request::Identifier::UserId(
                    info.user_id,
                )),
            },
        )?;

        let resp = self
            .client()
            .get_user(req)
            .await
            .map_err(map_status)?
            .into_inner();

        let user = resp.user.ok_or(AuthError::Other("no user in response".into()))?;
        Ok(convert_user(user))
    }

    async fn list_tokens(
        &self,
        access_token: &str,
        user_id: &str,
    ) -> Result<Vec<PersonalAccessToken>, AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::ListPersonalAccessTokensRequest {
                user_id: user_id.into(),
            },
        )?;

        let resp = self
            .client()
            .list_personal_access_tokens(req)
            .await
            .map_err(map_status)?
            .into_inner();

        Ok(resp.tokens.into_iter().map(convert_token).collect())
    }

    async fn create_token(
        &self,
        access_token: &str,
        user_id: &str,
        name: &str,
    ) -> Result<CreatedToken, AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::CreatePersonalAccessTokenRequest {
                user_id: user_id.into(),
                name: name.into(),
                scopes: vec![],
                expires_in_seconds: 0,
            },
        )?;

        let resp = self
            .client()
            .create_personal_access_token(req)
            .await
            .map_err(map_status)?
            .into_inner();

        let token = resp
            .token
            .ok_or(AuthError::Other("no token in response".into()))?;
        Ok(CreatedToken {
            token: convert_token(token),
            raw_token: resp.raw_token,
        })
    }

    async fn delete_token(
        &self,
        access_token: &str,
        token_id: &str,
    ) -> Result<(), AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::DeletePersonalAccessTokenRequest {
                token_id: token_id.into(),
            },
        )?;

        self.client()
            .delete_personal_access_token(req)
            .await
            .map_err(map_status)?;
        Ok(())
    }

    async fn update_username(
        &self,
        access_token: &str,
        user_id: &str,
        new_username: &str,
    ) -> Result<User, AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::UpdateUserRequest {
                user_id: user_id.into(),
                username: Some(new_username.into()),
            },
        )?;

        let resp = self
            .client()
            .update_user(req)
            .await
            .map_err(map_status)?
            .into_inner();

        let user = resp.user.ok_or(AuthError::Other("no user in response".into()))?;
        Ok(convert_user(user))
    }

    async fn change_password(
        &self,
        access_token: &str,
        user_id: &str,
        current_password: &str,
        new_password: &str,
    ) -> Result<(), AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::ChangePasswordRequest {
                user_id: user_id.into(),
                current_password: current_password.into(),
                new_password: new_password.into(),
            },
        )?;

        self.client()
            .change_password(req)
            .await
            .map_err(map_status)?;
        Ok(())
    }

    async fn add_email(
        &self,
        access_token: &str,
        user_id: &str,
        email: &str,
    ) -> Result<UserEmail, AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::AddEmailRequest {
                user_id: user_id.into(),
                email: email.into(),
            },
        )?;

        let resp = self
            .client()
            .add_email(req)
            .await
            .map_err(map_status)?
            .into_inner();

        let email = resp.email.ok_or(AuthError::Other("no email in response".into()))?;
        Ok(UserEmail {
            email: email.email,
            verified: email.verified,
        })
    }

    async fn remove_email(
        &self,
        access_token: &str,
        user_id: &str,
        email: &str,
    ) -> Result<(), AuthError> {
        let req = Self::authed_request(
            access_token,
            forage_grpc::RemoveEmailRequest {
                user_id: user_id.into(),
                email: email.into(),
            },
        )?;

        self.client()
            .remove_email(req)
            .await
            .map_err(map_status)?;
        Ok(())
    }
}

fn convert_organisations(
    organisations: Vec<forage_grpc::Organisation>,
    roles: Vec<String>,
) -> Vec<Organisation> {
    organisations
        .into_iter()
        .zip(roles)
        .map(|(org, role)| Organisation {
            organisation_id: org.organisation_id,
            name: org.name,
            role,
        })
        .collect()
}

fn convert_artifact(a: forage_grpc::Artifact) -> Artifact {
    let ctx = a.context.unwrap_or_default();
    let source = a.source.map(|s| ArtifactSource {
        user: s.user.filter(|v| !v.is_empty()),
        email: s.email.filter(|v| !v.is_empty()),
        source_type: s.source_type.filter(|v| !v.is_empty()),
        run_url: s.run_url.filter(|v| !v.is_empty()),
    });
    // Artifact proto does not carry git ref directly; git info comes from AnnotateRelease.
    // We leave git_ref as None for now.
    let destinations = a
        .destinations
        .into_iter()
        .map(|d| ArtifactDestination {
            name: d.name,
            environment: d.environment,
            type_organisation: if d.type_organisation.is_empty() {
                None
            } else {
                Some(d.type_organisation)
            },
            type_name: if d.type_name.is_empty() {
                None
            } else {
                Some(d.type_name)
            },
            type_version: if d.type_version == 0 {
                None
            } else {
                Some(d.type_version)
            },
        })
        .collect();
    Artifact {
        artifact_id: a.artifact_id,
        slug: a.slug,
        context: ArtifactContext {
            title: ctx.title,
            description: if ctx.description.as_deref() == Some("") {
                None
            } else {
                ctx.description
            },
            web: ctx.web.filter(|v| !v.is_empty()),
            pr: ctx.pr.filter(|v| !v.is_empty()),
        },
        source,
        git_ref: None,
        destinations,
        created_at: a.created_at,
    }
}

fn convert_member(m: forage_grpc::OrganisationMember) -> OrgMember {
    OrgMember {
        user_id: m.user_id,
        username: m.username,
        role: m.role,
        joined_at: m.joined_at.map(|ts| ts.to_string()),
    }
}

fn map_platform_status(status: tonic::Status) -> PlatformError {
    match status.code() {
        tonic::Code::Unauthenticated | tonic::Code::PermissionDenied => {
            PlatformError::NotAuthenticated
        }
        tonic::Code::NotFound => PlatformError::NotFound(status.message().into()),
        tonic::Code::Unavailable => PlatformError::Unavailable(status.message().into()),
        _ => PlatformError::Other(status.message().into()),
    }
}

fn platform_authed_request<T>(access_token: &str, msg: T) -> Result<Request<T>, PlatformError> {
    bearer_request(access_token, msg).map_err(PlatformError::Other)
}

#[async_trait::async_trait]
impl ForestPlatform for GrpcForestClient {
    async fn list_my_organisations(
        &self,
        access_token: &str,
    ) -> Result<Vec<Organisation>, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::ListMyOrganisationsRequest { role: String::new() },
        )?;

        let resp = self
            .org_client()
            .list_my_organisations(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        Ok(convert_organisations(resp.organisations, resp.roles))
    }

    async fn list_projects(
        &self,
        access_token: &str,
        organisation: &str,
    ) -> Result<Vec<String>, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::GetProjectsRequest {
                query: Some(forage_grpc::get_projects_request::Query::Organisation(
                    forage_grpc::OrganisationRef {
                        organisation: organisation.into(),
                    },
                )),
            },
        )?;

        let resp = self
            .release_client()
            .get_projects(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        Ok(resp.projects)
    }

    async fn list_artifacts(
        &self,
        access_token: &str,
        organisation: &str,
        project: &str,
    ) -> Result<Vec<Artifact>, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::GetArtifactsByProjectRequest {
                project: Some(forage_grpc::Project {
                    organisation: organisation.into(),
                    project: project.into(),
                }),
            },
        )?;

        let resp = self
            .release_client()
            .get_artifacts_by_project(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        Ok(resp.artifact.into_iter().map(convert_artifact).collect())
    }

    async fn create_organisation(
        &self,
        access_token: &str,
        name: &str,
    ) -> Result<String, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::CreateOrganisationRequest {
                name: name.into(),
            },
        )?;

        let resp = self
            .org_client()
            .create_organisation(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        Ok(resp.organisation_id)
    }

    async fn list_members(
        &self,
        access_token: &str,
        organisation_id: &str,
    ) -> Result<Vec<OrgMember>, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::ListMembersRequest {
                organisation_id: organisation_id.into(),
                page_size: 100,
                page_token: String::new(),
            },
        )?;

        let resp = self
            .org_client()
            .list_members(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        Ok(resp.members.into_iter().map(convert_member).collect())
    }

    async fn add_member(
        &self,
        access_token: &str,
        organisation_id: &str,
        user_id: &str,
        role: &str,
    ) -> Result<OrgMember, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::AddMemberRequest {
                organisation_id: organisation_id.into(),
                user_id: user_id.into(),
                role: role.into(),
            },
        )?;

        let resp = self
            .org_client()
            .add_member(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        let member = resp
            .member
            .ok_or(PlatformError::Other("no member in response".into()))?;
        Ok(convert_member(member))
    }

    async fn remove_member(
        &self,
        access_token: &str,
        organisation_id: &str,
        user_id: &str,
    ) -> Result<(), PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::RemoveMemberRequest {
                organisation_id: organisation_id.into(),
                user_id: user_id.into(),
            },
        )?;

        self.org_client()
            .remove_member(req)
            .await
            .map_err(map_platform_status)?;
        Ok(())
    }

    async fn update_member_role(
        &self,
        access_token: &str,
        organisation_id: &str,
        user_id: &str,
        role: &str,
    ) -> Result<OrgMember, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::UpdateMemberRoleRequest {
                organisation_id: organisation_id.into(),
                user_id: user_id.into(),
                role: role.into(),
            },
        )?;

        let resp = self
            .org_client()
            .update_member_role(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        let member = resp
            .member
            .ok_or(PlatformError::Other("no member in response".into()))?;
        Ok(convert_member(member))
    }

    async fn get_artifact_by_slug(
        &self,
        access_token: &str,
        slug: &str,
    ) -> Result<Artifact, PlatformError> {
        let req = platform_authed_request(
            access_token,
            forage_grpc::GetArtifactBySlugRequest {
                slug: slug.into(),
            },
        )?;

        let resp = self
            .release_client()
            .get_artifact_by_slug(req)
            .await
            .map_err(map_platform_status)?
            .into_inner();

        let artifact = resp
            .artifact
            .ok_or(PlatformError::NotFound("artifact not found".into()))?;
        Ok(convert_artifact(artifact))
    }

    async fn list_destinations(
        &self,
        _access_token: &str,
        _organisation: &str,
    ) -> Result<Vec<Destination>, PlatformError> {
        // DestinationService client not yet generated; return empty for now
        Ok(vec![])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_org(id: &str, name: &str) -> forage_grpc::Organisation {
        forage_grpc::Organisation {
            organisation_id: id.into(),
            name: name.into(),
            ..Default::default()
        }
    }

    fn make_artifact(slug: &str, ctx: Option<forage_grpc::ArtifactContext>) -> forage_grpc::Artifact {
        forage_grpc::Artifact {
            artifact_id: "a1".into(),
            slug: slug.into(),
            context: ctx,
            created_at: "2026-01-01".into(),
            ..Default::default()
        }
    }

    #[test]
    fn convert_organisations_pairs_orgs_with_roles() {
        let orgs = vec![make_org("o1", "alpha"), make_org("o2", "beta")];
        let roles = vec!["owner".into(), "member".into()];

        let result = convert_organisations(orgs, roles);
        assert_eq!(result.len(), 2);
        assert_eq!(result[0].name, "alpha");
        assert_eq!(result[0].role, "owner");
        assert_eq!(result[1].name, "beta");
        assert_eq!(result[1].role, "member");
    }

    #[test]
    fn convert_organisations_truncates_when_roles_shorter() {
        let orgs = vec![make_org("o1", "alpha"), make_org("o2", "beta")];
        let roles = vec!["owner".into()]; // only 1 role for 2 orgs

        let result = convert_organisations(orgs, roles);
        // zip truncates to shorter iterator
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].name, "alpha");
    }

    #[test]
    fn convert_organisations_empty() {
        let result = convert_organisations(vec![], vec![]);
        assert!(result.is_empty());
    }

    #[test]
    fn convert_artifact_with_full_context() {
        let a = make_artifact("my-api", Some(forage_grpc::ArtifactContext {
            title: "My API".into(),
            description: Some("A cool API".into()),
            ..Default::default()
        }));

        let result = convert_artifact(a);
        assert_eq!(result.slug, "my-api");
        assert_eq!(result.context.title, "My API");
        assert_eq!(result.context.description.as_deref(), Some("A cool API"));
    }

    #[test]
    fn convert_artifact_empty_description_becomes_none() {
        let a = make_artifact("my-api", Some(forage_grpc::ArtifactContext {
            title: "My API".into(),
            description: Some(String::new()),
            ..Default::default()
        }));

        let result = convert_artifact(a);
        assert!(result.context.description.is_none());
    }

    #[test]
    fn convert_artifact_missing_context_uses_defaults() {
        let a = make_artifact("my-api", None);

        let result = convert_artifact(a);
        assert_eq!(result.context.title, "");
        assert!(result.context.description.is_none());
    }

    #[test]
    fn convert_artifact_none_description_stays_none() {
        let a = make_artifact("my-api", Some(forage_grpc::ArtifactContext {
            title: "My API".into(),
            description: None,
            ..Default::default()
        }));

        let result = convert_artifact(a);
        assert!(result.context.description.is_none());
    }
}
