import io
import json
import zipfile
from pathlib import Path

import httpx
from fastapi import Request, HTTPException
from fastapi.templating import Jinja2Templates
from jinja2 import TemplateError

from metacatalog_api import core, models
from metacatalog_api.router.api.share import share_router


templates = Jinja2Templates(directory=Path(__file__).parent / 'templates')


def map_license_to_zenodo(license: models.License | None) -> str | None:
    """
    Map MetaCatalog license to Zenodo license identifier.
    Returns None if no mapping exists.
    """
    if license is None:
        return None
    
    license_mapping = {
        'CC BY 4.0': 'cc-by-4.0',
        'CC BY-SA 4.0': 'cc-by-sa-4.0',
        'CC BY-NC 4.0': 'cc-by-nc-4.0',
        'CC BY-NC-SA 4.0': 'cc-by-nc-sa-4.0',
        'ODbL': 'odbl-1.0',
        'ODC-by': 'odc-by-1.0',
        'dl-by-de/2.0': 'dl-de-by-2.0',
    }
    
    return license_mapping.get(license.short_title)


def validate_entry_for_zenodo(entry: models.Metadata) -> tuple[bool, list[str]]:
    """
    Validate that an entry has all required fields for Zenodo upload.
    Returns: (is_valid, missing_fields) where missing_fields is a list of error messages.
    """
    missing_fields = []
    
    # Title is required (but already required by MetaCatalog)
    if not entry.title or not entry.title.strip():
        missing_fields.append("Entry title is required")
    
    # At least one valid creator is required
    valid_creators = []
    
    # Check main author
    if entry.author:
        if entry.author.is_organisation:
            if entry.author.organisation_name and entry.author.organisation_name.strip():
                valid_creators.append(entry.author)
            else:
                missing_fields.append("Author organisation name is required")
        else:
            if (entry.author.first_name and entry.author.first_name.strip() and 
                entry.author.last_name and entry.author.last_name.strip()):
                valid_creators.append(entry.author)
            else:
                missing_fields.append("Author first name and last name are required")
    
    # Check co-authors
    for co_author in entry.coAuthors:
        if co_author.is_organisation:
            if co_author.organisation_name and co_author.organisation_name.strip():
                valid_creators.append(co_author)
        else:
            if (co_author.first_name and co_author.first_name.strip() and 
                co_author.last_name and co_author.last_name.strip()):
                valid_creators.append(co_author)
    
    if len(valid_creators) == 0:
        missing_fields.append("At least one valid creator (with complete name) is required")
    
    # License with valid Zenodo mapping is required
    zenodo_license = map_license_to_zenodo(entry.license)
    if not entry.license:
        missing_fields.append("Entry license is required")
    elif zenodo_license is None:
        missing_fields.append(f"License '{entry.license.short_title}' does not have a Zenodo mapping")
    
    is_valid = len(missing_fields) == 0
    return is_valid, missing_fields


def convert_entry_to_zenodo_metadata(entry: models.Metadata) -> dict:
    """
    Convert MetaCatalog entry to Zenodo metadata format.
    """
    # Build creators list
    creators = []
    
    # Add main author if valid
    if entry.author:
        if entry.author.is_organisation:
            if entry.author.organisation_name and entry.author.organisation_name.strip():
                creator = {
                    "name": entry.author.organisation_name
                }
                if entry.author.organisation_abbrev:
                    creator["name"] = f"{entry.author.organisation_name} ({entry.author.organisation_abbrev})"
                if entry.author.affiliation:
                    creator["affiliation"] = entry.author.affiliation
                if entry.author.orcid:
                    creator["orcid"] = entry.author.orcid
                creators.append(creator)
        else:
            if (entry.author.first_name and entry.author.first_name.strip() and 
                entry.author.last_name and entry.author.last_name.strip()):
                creator = {
                    "name": f"{entry.author.first_name} {entry.author.last_name}"
                }
                if entry.author.affiliation:
                    creator["affiliation"] = entry.author.affiliation
                if entry.author.orcid:
                    creator["orcid"] = entry.author.orcid
                creators.append(creator)
    
    # Add co-authors if valid
    for co_author in entry.coAuthors:
        if co_author.is_organisation:
            if co_author.organisation_name and co_author.organisation_name.strip():
                creator = {
                    "name": co_author.organisation_name
                }
                if co_author.organisation_abbrev:
                    creator["name"] = f"{co_author.organisation_name} ({co_author.organisation_abbrev})"
                if co_author.affiliation:
                    creator["affiliation"] = co_author.affiliation
                if co_author.orcid:
                    creator["orcid"] = co_author.orcid
                creators.append(creator)
        else:
            if (co_author.first_name and co_author.first_name.strip() and 
                co_author.last_name and co_author.last_name.strip()):
                creator = {
                    "name": f"{co_author.first_name} {co_author.last_name}"
                }
                if co_author.affiliation:
                    creator["affiliation"] = co_author.affiliation
                if co_author.orcid:
                    creator["orcid"] = co_author.orcid
                creators.append(creator)
    
    # Build metadata dict
    metadata = {
        "title": entry.title,
        "upload_type": "dataset",
        "description": entry.abstract or entry.title,
        "creators": creators,
    }
    
    # Add license
    zenodo_license = map_license_to_zenodo(entry.license)
    if zenodo_license:
        metadata["license"] = zenodo_license
    
    # Add keywords if available
    if entry.keywords:
        metadata["keywords"] = [kw.value for kw in entry.keywords if kw.value]
    
    # Add publication date
    if entry.publication:
        metadata["publication_date"] = entry.publication.strftime("%Y-%m-%d")
    
    # Add version
    metadata["version"] = str(entry.version)
    
    return {"metadata": metadata}


def create_zenodo_package(request: Request, entry_id: int) -> tuple[io.BytesIO, str]:
    """
    Create a Zenodo-specific package with README.md, metacatalog.json, 
    metadata exports, and data files.
    
    Returns:
        tuple: (zip_buffer, filename) where zip_buffer is a BytesIO object
    """
    # Get entry metadata
    entries = core.entries(ids=entry_id)
    if len(entries) == 0:
        raise HTTPException(status_code=404, detail=f"Entry of <ID={entry_id}> not found")
    
    entry = entries[0]
    
    # Create ZIP file in memory
    zip_buffer = io.BytesIO()
    
    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
        # 1. Generate and add README.md
        readme_content = f"# {entry.title}\n\n"
        
        if entry.abstract:
            readme_content += f"## Abstract\n\n{entry.abstract}\n\n"
        
        readme_content += "## About This Record\n\n"
        readme_content += "This record was automatically generated and uploaded by MetaCatalog API.\n\n"
        
        # Generate backlink URL from request
        base_url = f"{request.url.scheme}://{request.url.netloc}"
        entry_url = f"{base_url}/entries/{entry_id}"
        readme_content += f"[View this entry in MetaCatalog]({entry_url})\n"
        
        zip_file.writestr("README.md", readme_content)
        
        # 2. Add metacatalog.json to root
        entry_dict = entry.model_dump(mode='json')
        zip_file.writestr(
            "metacatalog.json",
            json.dumps(entry_dict, indent=2, default=str)
        )
        
        # 3. Add metadata exports to metadata/ folder
        # Add datacite.xml
        try:
            datacite_content = templates.get_template("datacite.xml").render(entry=entry)
            zip_file.writestr("metadata/datacite.xml", datacite_content)
        except TemplateError:
            # Skip if template fails, but continue
            pass
        
        # Add dublincore.xml
        try:
            dublincore_content = templates.get_template("dublincore.xml").render(entry=entry)
            zip_file.writestr("metadata/dublincore.xml", dublincore_content)
        except TemplateError:
            # Skip if template fails, but continue
            pass
        
        # 4. Add data files to data/ folder
        data_info = core.get_entry_data_file(entry_id)
        
        if data_info['error']:
            # Create error manifest
            manifest = {
                "type": "error",
                "error": data_info['error'],
                "description": "Data file could not be included in package"
            }
            zip_file.writestr("data/manifest.json", json.dumps(manifest, indent=2))
        elif data_info['is_stream']:

            csv_content = "".join(data_info['stream_generator']())
            zip_file.writestr(f"data/{data_info['filename']}", csv_content)
        elif data_info['file_path']:
            # File-based datasource - add file to ZIP
            zip_file.write(str(data_info['file_path']), f"data/{data_info['filename']}")
        else:
            # External or unsupported - create manifest
            if entry.datasource:
                if entry.datasource.type.name == "external":
                    manifest = {
                        "type": "external",
                        "url": entry.datasource.path,
                        "description": "External datasource - data not included in package"
                    }
                else:
                    manifest = {
                        "type": entry.datasource.type.name if entry.datasource.type else "unknown",
                        "path": entry.datasource.path,
                        "status": "unsupported",
                        "description": f"Datasource type '{entry.datasource.type.name if entry.datasource.type else 'unknown'}' is not supported for packaging"
                    }
                zip_file.writestr("data/manifest.json", json.dumps(manifest, indent=2))
    
    zip_buffer.seek(0)
    filename = f"entry_{entry_id}_package.zip"
    
    return zip_buffer, filename


@share_router.get('/share/zenodo/form')
def get_zenodo_form(entry_id: int, request: Request):
    """
    Zenodo Upload
    Get form for Zenodo upload
    """
    # Validate entry exists
    entries = core.entries(ids=entry_id)
    if len(entries) == 0:
        raise HTTPException(status_code=404, detail=f"Entry of <ID={entry_id}> not found")
    
    entry = entries[0]
    
    # Validate entry for Zenodo
    is_valid, missing_fields = validate_entry_for_zenodo(entry)
    
    if not is_valid:
        # Return form with error
        return {
            "fields": [
                {
                    "name": "error",
                    "type": "error",
                    "message": f"Entry is missing required fields for Zenodo upload: {', '.join(missing_fields)}"
                }
            ]
        }
    
    # Return form with token and sandbox checkbox
    return {
        "fields": [
            {
                "name": "zenodo_token",
                "type": "text",
                "label": "Zenodo Access Token",
                "required": True,
                "password": True,
                "help_text": "Your Zenodo personal access token. Create one at https://zenodo.org/account/settings/applications/"
            },
            {
                "name": "use_sandbox",
                "type": "checkbox",
                "label": "Use Sandbox (for testing)",
                "default": True,
                "help_text": "Upload to Zenodo sandbox instead of production"
            }
        ],
        "metadata_preview": False
    }


@share_router.post('/share/zenodo/submit')
async def submit_zenodo(entry_id: int, request: Request):
    """
    Submit Zenodo upload request
    """
    # Parse request body
    try:
        body = await request.json()
        zenodo_token = body.get('zenodo_token')
        use_sandbox = body.get('use_sandbox', True)
    except (json.JSONDecodeError, ValueError, TypeError, KeyError) as e:
        raise HTTPException(status_code=400, detail="Invalid request body. Expected JSON with 'zenodo_token' and optional 'use_sandbox' fields.") from e
    
    if not zenodo_token:
        raise HTTPException(status_code=400, detail="zenodo_token is required")
    
    # Strip whitespace from token
    zenodo_token = zenodo_token.strip()
    
    # Validate entry exists
    entries = core.entries(ids=entry_id)
    if len(entries) == 0:
        raise HTTPException(status_code=404, detail=f"Entry of <ID={entry_id}> not found")
    
    entry = entries[0]
    
    # Validate entry for Zenodo
    is_valid, missing_fields = validate_entry_for_zenodo(entry)
    if not is_valid:
        raise HTTPException(
            status_code=400,
            detail=f"Entry is missing required fields for Zenodo upload: {', '.join(missing_fields)}"
        )
    
    # Determine Zenodo base URL
    if use_sandbox:
        zenodo_base_url = "https://sandbox.zenodo.org/api"
    else:
        zenodo_base_url = "https://zenodo.org/api"
    
    # Prepare headers for Zenodo API
    headers = {
        "Authorization": f"Bearer {zenodo_token}",
        "Content-Type": "application/json"
    }
    
    # Create Zenodo-specific package
    zip_buffer, zip_filename = create_zenodo_package(request, entry_id)
    zip_data = zip_buffer.read()
    zip_buffer.close()
    
    try:
        # Step 1: Create empty deposition
        async with httpx.AsyncClient() as client:
            create_response = await client.post(
                f"{zenodo_base_url}/deposit/depositions",
                json={},
                headers=headers,
                timeout=30.0
            )
            
            if create_response.status_code not in [200, 201]:
                error_detail = create_response.text
                try:
                    error_json = create_response.json()
                    error_detail = error_json.get('message', error_json.get('status', error_detail))
                    # Log full error response for debugging
                    if error_json:
                        error_detail = f"{error_detail} (Full response: {error_json})"
                except (json.JSONDecodeError, ValueError):
                    # Response is not JSON, use text as-is
                    pass
                
                # Provide helpful error messages for common issues
                if create_response.status_code == 401:
                    error_detail = "Invalid or expired Zenodo access token. Please check your token and try again."
                elif create_response.status_code == 403:
                    error_detail = f"Permission denied. Please ensure: (1) Your token is for the correct environment (sandbox vs production), (2) The token has 'deposit:write' scope, and (3) The token is valid. Error: {error_detail}"
                
                raise HTTPException(
                    status_code=create_response.status_code,
                    detail=f"Failed to create Zenodo deposition: {error_detail}"
                )
            
            deposition = create_response.json()
            bucket_url = deposition["links"]["bucket"]
            deposition_id = deposition["id"]
            
            # Step 2: Upload file to bucket
            upload_headers = {
                "Authorization": f"Bearer {zenodo_token}"
            }
            
            upload_response = await client.put(
                f"{bucket_url}/{zip_filename}",
                content=zip_data,
                headers=upload_headers,
                timeout=60.0
            )
            
            if upload_response.status_code not in [200, 201]:
                error_detail = upload_response.text
                try:
                    error_json = upload_response.json()
                    error_detail = error_json.get('message', error_detail)
                except (json.JSONDecodeError, ValueError):
                    # Response is not JSON, use text as-is
                    pass
                raise HTTPException(
                    status_code=upload_response.status_code,
                    detail=f"Failed to upload file to Zenodo: {error_detail}"
                )
            
            # Step 3: Add metadata
            zenodo_metadata = convert_entry_to_zenodo_metadata(entry)
            
            metadata_response = await client.put(
                f"{zenodo_base_url}/deposit/depositions/{deposition_id}",
                json=zenodo_metadata,
                headers=headers,
                timeout=30.0
            )
            
            if metadata_response.status_code not in [200, 201]:
                error_detail = metadata_response.text
                try:
                    error_json = metadata_response.json()
                    error_detail = error_json.get('message', error_detail)
                except (json.JSONDecodeError, ValueError):
                    # Response is not JSON, use text as-is
                    pass
                raise HTTPException(
                    status_code=metadata_response.status_code,
                    detail=f"Failed to update Zenodo metadata: {error_detail}"
                )
            
            # Get updated deposition
            final_deposition = metadata_response.json()
            
            # Extract pre-reserved DOI (if available)
            prereserved_doi = None
            metadata = final_deposition.get("metadata", {})
            if "prereserve_doi" in metadata:
                prereserved_doi = metadata["prereserve_doi"].get("doi")
            
            # Step 4: Return response with structured fields
            zenodo_links = final_deposition.get("links", {})
            response = {
                "success": True,
                "message": "Entry successfully uploaded to Zenodo.",
                "warning": "The entry is in draft mode. Use the publish link below to make it public and activate the DOI.",
                "links": {
                    "View": zenodo_links.get("html"),
                    "Edit": zenodo_links.get("edit"),
                    "Publish": zenodo_links.get("publish"),
                }
            }
            
            # Add pre-reserved DOI if available
            if prereserved_doi:
                response["prereserved_doi"] = prereserved_doi
                response["message"] = f"Entry successfully uploaded to Zenodo. Pre-reserved DOI: {prereserved_doi} (will be activated upon publishing)."
            
            return response
            
    except HTTPException:
        raise
    except httpx.HTTPError as e:
        raise HTTPException(
            status_code=500,
            detail=f"Network error while communicating with Zenodo: {e!s}"
        ) from e
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Unexpected error during Zenodo upload: {e!s}"
        ) from e
