"""
GitHub Integration for Custom Algorithm Uploads
================================================

This module handles uploading custom algorithms to a dedicated GitHub repository
as separate branches for admin review before merging into the main algorithm library.

Features:
- Create per-contributor branch for each algorithm submission
- Upload algorithm file with contributor credit metadata
- Create PR with full attribution and credit information
- Track submission status
- Contributor credits preserved through review and release cycle

Author: MHA Flow Development Team
License: MIT
"""

import os
import json
import base64
import requests
from typing import Dict, Optional, Any
from datetime import datetime
from pathlib import Path


class GitHubAlgorithmUploader:
    """Upload custom algorithms to GitHub for review"""
    
    def __init__(self, token: Optional[str] = None, repo: Optional[str] = None):
        """
        Initialize GitHub uploader.
        
        Parameters
        ----------
        token : str, optional
            GitHub personal access token (or set GITHUB_TOKEN env var)
        repo : str, optional
            Repository in format 'owner/repo' (or set GITHUB_REPO env var)
        """
        self.token = token or os.environ.get('GITHUB_TOKEN')
        self.repo = repo or os.environ.get('GITHUB_REPO', 'yourusername/mha-custom-algorithms')
        
        if not self.token:
            raise ValueError("GitHub token required. Set GITHUB_TOKEN environment variable or pass token parameter.")
        
        self.api_base = 'https://api.github.com'
        self.headers = {
            'Authorization': f'token {self.token}',
            'Accept': 'application/vnd.github.v3+json',
            'Content-Type': 'application/json'
        }
    
    def create_branch(self, branch_name: str, from_branch: str = 'main') -> Dict[str, Any]:
        """
        Create a new branch for the custom algorithm.
        
        Parameters
        ----------
        branch_name : str
            Name of the new branch
        from_branch : str
            Base branch to create from (default: 'main')
            
        Returns
        -------
        result : dict
            Status and branch information
        """
        try:
            # Get SHA of the base branch
            ref_url = f'{self.api_base}/repos/{self.repo}/git/ref/heads/{from_branch}'
            ref_response = requests.get(ref_url, headers=self.headers)
            
            if ref_response.status_code != 200:
                return {
                    'success': False,
                    'error': f'Failed to get base branch: {ref_response.json()}'
                }
            
            base_sha = ref_response.json()['object']['sha']
            
            # Create new branch
            create_url = f'{self.api_base}/repos/{self.repo}/git/refs'
            create_data = {
                'ref': f'refs/heads/{branch_name}',
                'sha': base_sha
            }
            
            create_response = requests.post(create_url, headers=self.headers, json=create_data)
            
            if create_response.status_code in [200, 201]:
                return {
                    'success': True,
                    'branch': branch_name,
                    'sha': base_sha
                }
            else:
                return {
                    'success': False,
                    'error': f'Failed to create branch: {create_response.json()}'
                }
                
        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }
    
    def upload_file(self, 
                   file_path: str, 
                   file_content: str, 
                   branch: str,
                   commit_message: str) -> Dict[str, Any]:
        """
        Upload algorithm file to GitHub branch.
        
        Parameters
        ----------
        file_path : str
            Path in repository where file should be saved
        file_content : str
            Content of the file
        branch : str
            Branch to commit to
        commit_message : str
            Commit message
            
        Returns
        -------
        result : dict
            Status and commit information
        """
        try:
            # Check if file exists
            content_url = f'{self.api_base}/repos/{self.repo}/contents/{file_path}'
            params = {'ref': branch}
            get_response = requests.get(content_url, headers=self.headers, params=params)
            
            # Encode content
            content_bytes = file_content.encode('utf-8')
            encoded_content = base64.b64encode(content_bytes).decode('utf-8')
            
            # Prepare data
            data = {
                'message': commit_message,
                'content': encoded_content,
                'branch': branch
            }
            
            # If file exists, we need the SHA for update
            if get_response.status_code == 200:
                data['sha'] = get_response.json()['sha']
            
            # Upload/update file
            put_response = requests.put(content_url, headers=self.headers, json=data)
            
            if put_response.status_code in [200, 201]:
                return {
                    'success': True,
                    'commit': put_response.json()['commit']['sha'],
                    'file_url': put_response.json()['content']['html_url']
                }
            else:
                return {
                    'success': False,
                    'error': f'Failed to upload file: {put_response.json()}'
                }
                
        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }
    
    def create_pull_request(self,
                          branch: str,
                          title: str,
                          body: str,
                          base_branch: str = 'main') -> Dict[str, Any]:
        """
        Create a pull request for the custom algorithm.
        
        Parameters
        ----------
        branch : str
            Source branch with the algorithm
        title : str
            PR title
        body : str
            PR description
        base_branch : str
            Target branch (default: 'main')
            
        Returns
        -------
        result : dict
            Status and PR information
        """
        try:
            pr_url = f'{self.api_base}/repos/{self.repo}/pulls'
            pr_data = {
                'title': title,
                'body': body,
                'head': branch,
                'base': base_branch
            }
            
            pr_response = requests.post(pr_url, headers=self.headers, json=pr_data)
            
            if pr_response.status_code == 201:
                pr_info = pr_response.json()
                return {
                    'success': True,
                    'pr_number': pr_info['number'],
                    'pr_url': pr_info['html_url'],
                    'state': pr_info['state']
                }
            else:
                return {
                    'success': False,
                    'error': f'Failed to create PR: {pr_response.json()}'
                }
                
        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }
    
    def submit_algorithm(self,
                        algorithm_name: str,
                        algorithm_file_path: str,
                        algorithm_content: str,
                        metadata: Dict[str, Any],
                        author: str) -> Dict[str, Any]:
        """
        Complete workflow to submit a custom algorithm with contributor credit.
        
        Creates a per-contributor branch, uploads the algorithm with full
        attribution, and optionally creates a PR for admin review.
        
        Parameters
        ----------
        algorithm_name : str
            Name of the algorithm
        algorithm_file_path : str
            Local path to algorithm file
        algorithm_content : str
            Content of the algorithm file
        metadata : dict
            Algorithm metadata (description, category, parameters, credit info)
        author : str
            Username of the submitter
            
        Returns
        -------
        result : dict
            Complete submission result with branch, file URL, and credit info
        """
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_author = author.lower().replace(' ', '_').replace('@', '_at_')
        safe_algo = algorithm_name.lower().replace(' ', '_')
        
        # Per-contributor branch: contrib/<author>/<algorithm>
        branch_name = f'contrib/{safe_author}/{safe_algo}'
        
        # Step 1: Try to create the per-contributor branch
        branch_result = self.create_branch(branch_name)
        if not branch_result['success']:
            # Branch may already exist (re-submission), that's okay
            if 'already exists' in str(branch_result.get('error', '')).lower() or \
               'reference already exists' in str(branch_result.get('error', '')).lower():
                pass  # Use existing branch
            else:
                # Fallback to shared custom-algorithms branch
                branch_name = 'custom-algorithms'
        
        # Step 2: Upload algorithm file with credit header
        credit_header = f'''"""
Algorithm: {algorithm_name}
Contributed by: {author}
Email: {metadata.get('email', 'N/A')}
Submission Date: {datetime.now().isoformat()}
Category: {metadata.get('category', 'Custom')}
Description: {metadata.get('description', 'Custom algorithm contribution')}

This algorithm was contributed by {author} and is pending review.
Upon approval and release, full credit will be given to the contributor.
"""

'''
        credited_content = credit_header + algorithm_content
        
        unique_filename = f'{safe_algo}_{safe_author}_{timestamp}.py'
        repo_path = f'mha_toolbox/algorithms/custom/{unique_filename}'
        
        commit_message = (
            f'Add custom algorithm: {algorithm_name}\n\n'
            f'Contributed by: {author}\n'
            f'Category: {metadata.get("category", "Custom")}\n'
            f'Description: {metadata.get("description", "")[:200]}'
        )
        
        upload_result = self.upload_file(
            file_path=repo_path,
            file_content=credited_content,
            branch=branch_name,
            commit_message=commit_message
        )
        
        if not upload_result['success']:
            return upload_result
        
        # Step 3: Upload metadata JSON with full contributor credit
        credit_metadata = {
            **metadata,
            'contributor_credit': {
                'author': author,
                'email': metadata.get('email', ''),
                'submitted_at': datetime.now().isoformat(),
                'branch': branch_name,
                'algorithm_file': repo_path,
                'credit_text': f'Algorithm contributed by {author}',
                'acknowledgement': (
                    f'This algorithm was contributed by {author}. '
                    f'When included in a release, {author} will be credited '
                    f'as the original contributor in the library documentation '
                    f'and algorithm registry.'
                )
            },
            'parameter_mapping': metadata.get('parameter_mapping', {}),
        }
        
        metadata_path = f'mha_toolbox/algorithms/custom/{safe_algo}_{safe_author}_{timestamp}_metadata.json'
        
        self.upload_file(
            file_path=metadata_path,
            file_content=json.dumps(credit_metadata, indent=2),
            branch=branch_name,
            commit_message=f'Add metadata for {algorithm_name} by {author}'
        )
        
        # Step 4: Create PR if using per-contributor branch
        pr_result = {'success': False}
        if branch_name.startswith('contrib/'):
            pr_body = self._generate_pr_body(algorithm_name, author, metadata)
            pr_result = self.create_pull_request(
                branch=branch_name,
                title=f'[Custom Algorithm] {algorithm_name} by {author}',
                body=pr_body,
                base_branch='main'
            )
        
        return {
            'success': True,
            'branch': branch_name,
            'algorithm_file_url': upload_result.get('file_url'),
            'pr_url': pr_result.get('pr_url'),
            'pr_number': pr_result.get('pr_number'),
            'contributor_credit': credit_metadata['contributor_credit'],
            'message': (
                f'Algorithm {algorithm_name} uploaded to branch "{branch_name}" for review. '
                f'Contributor credit for {author} has been recorded. '
                f'Upon approval and next release, {author} will be credited.'
            )
        }
    
    def _generate_pr_body(self, algorithm_name: str, author: str, 
                         metadata: Dict[str, Any]) -> str:
        """Generate a rich PR body with contributor credit and algorithm details."""
        param_mapping = metadata.get('parameter_mapping', {})
        param_section = ""
        if param_mapping:
            param_lines = []
            for custom, info in param_mapping.items():
                if isinstance(info, dict):
                    standard = info.get('suggested', info.get('standard', '?'))
                    confidence = info.get('confidence', 'unknown')
                    param_lines.append(f"| `{custom}` | `{standard}` | {confidence} |")
                else:
                    param_lines.append(f"| `{custom}` | `{info}` | - |")
            if param_lines:
                param_section = (
                    "\n## Parameter Mapping\n\n"
                    "| Custom Param | Standard Param | Confidence |\n"
                    "|---|---|---|\n" +
                    "\n".join(param_lines)
                )
        
        return f"""## New Custom Algorithm: {algorithm_name}

### Contributor
- **Author:** {author}
- **Email:** {metadata.get('email', 'N/A')}
- **Date:** {datetime.now().strftime('%Y-%m-%d')}

### Algorithm Details
- **Category:** {metadata.get('category', 'Custom')}
- **Description:** {metadata.get('description', 'No description provided')}
{param_section}

### Reviewer Checklist
- [ ] Algorithm code reviewed for correctness
- [ ] No security issues found
- [ ] Parameter mapping verified
- [ ] Algorithm tested with benchmark functions
- [ ] Contributor credit added to CONTRIBUTORS.md

### Credit
> Upon merging, {author} will be credited as the contributor of {algorithm_name}
> in the algorithm registry and library documentation.

---
*Auto-generated by MHA Flow Custom Algorithm Upload System*
"""
    
    def get_submission_status(self, pr_number: int) -> Dict[str, Any]:
        """
        Check the status of a submitted algorithm.
        
        Parameters
        ----------
        pr_number : int
            Pull request number
            
        Returns
        -------
        status : dict
            Current status of the submission
        """
        try:
            pr_url = f'{self.api_base}/repos/{self.repo}/pulls/{pr_number}'
            pr_response = requests.get(pr_url, headers=self.headers)
            
            if pr_response.status_code == 200:
                pr_data = pr_response.json()
                return {
                    'success': True,
                    'state': pr_data['state'],
                    'merged': pr_data['merged'],
                    'mergeable': pr_data.get('mergeable'),
                    'url': pr_data['html_url'],
                    'created_at': pr_data['created_at'],
                    'updated_at': pr_data['updated_at']
                }
            else:
                return {
                    'success': False,
                    'error': f'Failed to get PR status: {pr_response.json()}'
                }
                
        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }
