from aleph_secrets_manager.src.port import Secret, SecretsManagerPort

import requests
import logging
from nacl import public, encoding
from base64 import b64encode
from pydantic import BaseModel


class GitHubPublicKey(BaseModel): 
    id: str
    value: str


# Guide: https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094
# REST API Reference: https://docs.github.com/en/rest/actions?apiVersion=2022-11-28#secrets
class GitHubSecretsManager(SecretsManagerPort): 
    def __init__(self, org: str, repo: str, auth_token: str):
        self.base_url = "https://api.github.com"
        self.org = org
        self.repo = repo
        self.token = auth_token
        self.headers = {
            "Authorization": f"Bearer {self.token}", 
            "X-GitHub-Api-Version": "2022-11-28"
        }
        raw_public_key = self._get_public_key(
            base_url=self.base_url, 
            org=self.org, 
            repo=self.repo, 
            auth_token=self.token
        )
        self.public_key = GitHubPublicKey(id=raw_public_key['key_id'], value=raw_public_key['key'])
        

    def _encrypt(self, public_key: str, unecrypted_value: str) -> str: 
        """Encrypt a secret value using the repo public key"""

        encoding_system = "utf-8"
        public_key_obj = public.PublicKey(public_key.encode(encoding_system), encoder=encoding.Base64Encoder())
        sealed_box = public.SealedBox(public_key_obj)
        encrypted_value = sealed_box.encrypt(unecrypted_value.encode(encoding_system))
        
        return b64encode(encrypted_value).decode(encoding_system)

    def _get_public_key(self, base_url: str, org: str, repo: str, auth_token: str):
        url = f"{base_url}/repos/{org}/{repo}/actions/secrets/public-key"
        headers = {"Authorization": f"Bearer {auth_token}"}
        
        response = requests.get(url=url, headers=headers, timeout=60)
        if response.status_code != 200: 
            logging.error(f"Response: {response.json()}")
            raise IOError(f"❌ Failed to get public key for {org} ({response.status_code}): \n{response.json()}")

        public_key_json = response.json()
        return public_key_json
    
    def list_all_secrets(self):
        """Retrieve all environment and repo secrets"""
        secrets = []
        url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets"
        
        response = requests.get(url=url, headers=self.headers, timeout=60)
        if not response.ok: 
            logging.error(f"Response: {response.json()}")
            raise IOError(f"❌ Failed to list secrets for {self.org} ({response.status_code}): \n{response.json()}")

        secrets += response.json()["secrets"]
        return [s['name'] for s in secrets]
    
    def write_secret(self, secret: Secret):
        """Only repository secrets should be written"""

        encrypted_value: str = self._encrypt(self.public_key.value, secret.value)
        url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{secret.key}"
        data = {
            "encrypted_value": encrypted_value,
            "key_id": self.public_key.id
        }

        response = requests.put(url=url, headers=self.headers, timeout=60, json=data)
        if not response.ok: 
            logging.error(f"Response: {response.json()}")
            raise IOError(f"❌ Failed to write secret {secret.key} ({response.status_code}): \n{response.json()}")

    def read_secret(self, key):
        """GitHub does not allow reading of secrets"""

        url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{key}"
        response = requests.get(url=url, headers=self.headers, timeout=60)
        if not response.ok: 
            logging.error(f"Response: {response.json()}")
            raise IOError(f"❌ Failed to read secret {key} ({response.status_code}): \n{response.json()}")
        
        return Secret(key=response.json()['name'], value="")
        
    def delete_secret(self, key):
        url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{key}"
        response = requests.delete(url=url, headers=self.headers, timeout=60)
        if not response.ok: 
            logging.error(f"Response: {response.json()}")
            if response.status_code == 404: 
                raise ValueError(f"❌ Failed to delete secret {key}: Does not exist")
            raise IOError(f"❌ Failed to delete secret {key} ({response.status_code}): \n{response.json()}")
        
    def read_all_secrets(self):
        secret_strings = self.list_all_secrets()
        return [self.read_secret(key) for key in secret_strings]