# routers/drafts.py
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi import   Depends, Response
from fastapi.responses import JSONResponse
import json
import time
from percolate.services import MinioService, S3Service
from percolate.api.routes.auth import get_api_key, get_current_token
from pydantic import BaseModel, Field
import typing
import uuid
from percolate.services import PostgresService
from percolate.models.p8 import IndexAudit
from percolate.utils import logger
from apscheduler.triggers.cron import CronTrigger

import traceback
from percolate.utils.studio import Project, apply_project
from fastapi import   Depends, File, UploadFile
import percolate as p8
import datetime
from percolate.models.p8.types import Schedule
from percolate.utils import try_parse_base64_dict
import time


class ScheduleCreate(BaseModel):
    """Request model for creating a schedule."""
    userid: str = Field(..., description="User id associated with schedule")
    name: str = Field(..., description="Task to execute")
    spec: dict = Field(..., description="The task spec can be any json but we have an internal protocol. LLM instructions are valid should use a system_prompt attribute")
    schedule: str = Field(..., description="Cron schedule string, e.g. '0 0 * * *'")

router = APIRouter()

@router.post("/env/sync")
async def sync_env(user: dict = Depends(get_api_key)):
    """sync env adds whatever keys you have in your environment your database instance
    This is used on database setup or if keys are missing in database sessions
    """
    return Response(content=json.dumps({'status':'ok'}))


class AddApiRequest(BaseModel):
    uri: str = Field(description="Add the uri to the openapi.json for the API you want to add")
    token: typing.Optional[str] = Field(description="Add an optional bearer token or API key for API access")
    verbs: typing.Optional[str] = Field(description="A comma-separated list of verbs e.g. get,post to filter endpoints by when adding endpoints")
    endpoint_filter: typing.Optional[typing.List[str]] = Field(description="A list of endpoints to filter by when adding endpoints")
    
@router.post("/add/api")
async def add_api( add_request:AddApiRequest,  user: dict = Depends(get_api_key)):
    """add apis to Percolate
    """
    return Response(content=json.dumps({'status':'ok'}))

class AddAgentRequest(BaseModel):
    name: str = Field(description="A unique entity name, fully qualified by namespace or use 'public' as default" )
    functions: dict = Field(description="A mapping of function names in Percolate with a description of how the function is useful to you")
    spec: dict = Field(description="The Json spec of your agents structured response e.g. from a Pydantic model")
    description: str = Field(description="Your agent description - acts as a system prompt")
    
    
@router.post("/add/agent")
async def add_agent( add_request:AddAgentRequest,  user: dict = Depends(get_api_key)):
    """add agents to Percolate. Agents require a Json Schema for any structured response you want to use, a system prompt and a dict/mapping of external registered functions.
    Functions can be registered via the add APIs endpoint.
    """
    return Response(content=json.dumps({'status':'ok'}))

@router.post("/add/project")
async def add_project( project: Project,  user: dict = Depends(get_api_key)):
    """Post the project yaml/json file to apply the settings. This can be used to add apis, agents and models. 
    
    - If you have set environment keys in your API we will sync these to your database if the `sync-env` flag is set in the project options
    - If you want to index the Percolation documentation set the flag `index-docs`
    """
    results = apply_project(project)
    return Response(content=json.dumps(results))


@router.get("/slow-endpoint", include_in_schema=False)
async def slow_response(user: dict = Depends(get_current_token)):
    """a test utility"""
    import time
    time.sleep(10)  # Simulate a delay
    return {"message": "This response was delayed by 10 seconds"}



class IndexRequest(BaseModel):
    """a request to update the indexes for entities by full name"""
    entity_full_name: str = Field(description="The full entity name - optionally omit for public namespace")

 
@router.post("/index/", response_model=IndexAudit)
async def index_entity(request: IndexRequest, background_tasks: BackgroundTasks, sleep_seconds:int=7, user: dict = Depends(get_api_key))->IndexAudit:
    """index entity and get an audit log id to check status
    the index is created as a background tasks and we respond with an id ref that can be used in the get/
    we sleep for n seconds to allow records to flush after trigger
    """
    id=uuid.uuid1()
    s = PostgresService(IndexAudit)
    logger.debug(f"Handling index request {request=} {sleep_seconds=}")
    time.sleep(sleep_seconds)
    try:
        record = IndexAudit(id=id, model_name='percolate', entity_full_name=request.entity_full_name, metrics={}, status="REQUESTED", message="Indexed requested")
        s.update_records(record)
        """todo create an audit record pending and use that in the api response"""
        background_tasks.add_task(s.index_entity_by_name, request.entity_full_name, id=id)
        return record
    except Exception as e:
        """handle api errors"""
        logger.warning(f"/admin/index {traceback.format_exc()}")
        record = IndexAudit(id=id,model_name='percolate',entity_full_name=request.entity_full_name, metrics={}, status="ERROR", message=str(e))
        """log the error"""
        s.update_records(record)
        raise HTTPException(status_code=500, detail="Failed to manage the index")
    
@router.get("/index/{id}", response_model=IndexAudit)
async def get_index(id: uuid.UUID, user: dict = Depends(get_api_key)) -> IndexAudit:
    """
    request the status of the index by id
    """
    #todo - proper error handling
    records = PostgresService.get_by_id(id)
    if records:
        return records
    """TODO error not found"""
    return {}



@router.post("/content/bookmark")
async def upload_uri(request : dict,
                     background_tasks: BackgroundTasks, 
                     task_id: str = None, 
                     user_id:str=None,
                     device_info:str =None, 
                     is_public_resource:bool = True,
                     expand_resource:bool=False,
                     token: dict = Depends(get_current_token)):
    """book mark uris the same way we upload file content. Resources are assumed public by default e.g. a public we reference but can be "owned" too
    A task_id is a thread id for a session parent e.g. a chat with actions pinned. but by default we just pin to the daily diary
    """

    from percolate.models.p8 import Resources, SessionResources, Session
    
    """quickly audit the bookmark and do the rest as a background task
    im likely to make resources and task resources first class citizens and also create a user session model and functions that do this in the database
    """
    
    """compatibility with modes"""
    user_id = user_id or request.get('user_id')
    #
    """any alias is fine"""
    uri = request.get('uri') or request.get('url')
    intent =  request.get('description') or request.get('summary') or request.get('content')
    resource_title = request.get('title') or request.get('name')
    """get device info from the base64string"""
    device_info:dict = try_parse_base64_dict(device_info) or {}
  
  
    if not task_id:
        task = Session.daily_diary_entry(user_id,query=uri, metadata=device_info)
        task_id = task.id
    else:
        task = Session.task_thread_entry(thread_id=task_id, userid=user_id, query=uri, metadata=device_info)
    
    """we are always auditing intent"""
    p8.repository(Session).update_records(task)   
        
    def index_resources():
        try: 
      
            """upsert the resource - allowing for aliases at least for now"""
             #the intent is the default but the user does not 'own' this public resource and we can process resources in the background - the session stores the user intent
            head =resources = Resources(name=resource_title,
                                        content=intent,
                                        summary = intent, 
                                        uri = uri,
                                        userid=None if is_public_resource else user_id)    
            length = 1
            if expand_resource:
                """we attempt to fetch the resource in markdown for the default provider"""
                resources = Resources.chunked_resource(uri,
                                                       try_markdown=True,
                                                       name=resource_title, userid= None if is_public_resource else user_id) 
                head = resources[0]
                length = len(resources)
            
            """link the resource to sessions
            We store only the head resource in the session for IIR for now
            """
            tr = SessionResources(resource_id=head.id, session_id=task_id,count=length)
        
            """for now we insert seps but in future we will have a function for this"""
            p8.repository(Resources).update_records(resources)
            p8.repository(SessionResources).update_records(tr)
            
            logger.debug(f"Saved task resource {tr=}")
        except:
            logger.warning(f"Failing background task")
            logger.warning(f"{traceback.format_exc()}")
            """todo implement failure system logs"""
            

    """background task"""
    background_tasks.add_task(index_resources)
       
    logger.info(f"{request=}")
       
    return JSONResponse({"status":f'received uri {uri}'})

@router.post("/content/upload")
async def upload_file(background_tasks: BackgroundTasks,
                      file: UploadFile = File(...),
                      task_id: str = None,
                      add_resource: bool = True, 
                      user_id:str = None, 
                      device_info:str =None,
                      token: dict = Depends(get_current_token)):
    """
    Uploads a file to S3 storage and optionally stores it as a file resource which is indexed.
    Files are stored under the task_id folder structure.
    
    Args:
        file: The file to upload
        task_id: The task ID to associate with the file, defaults to "default"
        add_resource: Whether to add the file as a database resource for content indexing
        user: The authenticated user (injected by dependency)
    
    Returns:
        JSON with the filename and status message
    """
    from percolate.models import Resources,Session,SessionResources
    device_info = try_parse_base64_dict(device_info)
                 
    def index_resource(file_upload_result:dict,task_id:str=None):
        """given a file upload result which provides e.g. the key, index the resource"""
        
        try:         
            logger.debug(f"indexing {file_upload_result=}")
            uri = file_upload_result['uri']
            if not task_id:
                """the user can upload either in a session context or we just pin to daily activity"""
                task = Session.daily_diary_entry(userid=user_id,query= uri, metadata=device_info)
                task_id = task.id
            else:
                task = Session.task_thread_entry(thread_id=task_id, userid=user_id, query=uri, metadata=device_info)
                
            """we are always auditing intent"""
            p8.repository(Session).update_records(task)   

            """TODO in future push down S3 urls and add a provider that the chunker and maybe the database can use"""
            resources = Resources.chunked_resource(name=file.filename, uri=uri,userid=user_id)
            
            if resources:
                _ = p8.repository(Resources).update_records(resources)
                """resources can be stored as chunked but we store a ref to the head only"""
                tr = SessionResources(resource_id=resources[0].id, session_id=task_id,count=len(resources))
                """for now we insert seps but in future we will have a function for this"""
                p8.repository(SessionResources).update_records(tr)
                
                logger.debug(f"uploaded {len(resources)} resource chunks ref={uri}")
        except:
            logger.warning(f"Failing background task")
            logger.warning(f"{traceback.format_exc()}")     
    
    try:
        
        # Upload to S3 using put_object with bytes
        s3_service = S3Service()
        print("USERID", user_id)
        path = f'users/{user_id}/' if user_id else ''
        """todo still deciding on a file path scheme"""
        path += f"{task_id or 'default'}"
        
        result = s3_service.upload_file(
            project_name=path,
            file_name=file.filename,
            file_content=file.file,
            content_type=file.content_type,
            fetch_presigned_url=True
        )
        
        if add_resource:
            background_tasks.add_task(index_resource, file_upload_result=result,task_id=task_id)
        
        logger.info(f"Uploaded file {result['key']} to S3 successfully")
        return JSONResponse({
            "key": result["key"],
            "filename": result["name"],
            "task_id": task_id,
            "size": result["size"],
            "content_type": result["content_type"],
            "last_modified": result["last_modified"],
            "etag": result["etag"],
            "path": path,
            "message": "Uploaded successfully to S3"
        })
    except Exception as e:
        logger.error(f"File upload failed: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")

@router.get("/content/files")
async def list_files(task_id: str = "default", prefix: str = None, user: dict = Depends(get_current_token)):
    """
    Lists files stored in S3 under the specified task_id.
    
    Args:
        task_id: The task ID folder to list files from, defaults to "default"
        prefix: Additional prefix to filter files within the task_id folder
        user: The authenticated user (injected by dependency)
    
    Returns:
        JSON list of files with metadata
    """
    try:
        # List files from S3
        s3_service = S3Service()
        files = s3_service.list_files(
            project_name=task_id,
            prefix=prefix
        )
        
        return {
            "task_id": task_id,
            "files": files,
            "count": len(files)
        }
    except Exception as e:
        logger.error(f"Failed to list files: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=500, detail=f"Failed to list files: {str(e)}")

@router.get("/content/file/{task_id}/{filename:path}")
async def get_file(task_id: str, filename: str, prefix: str = None, user: dict = Depends(get_current_token)):
    """
    Retrieves a file from S3 storage.
    
    Args:
        task_id: The task ID/project name associated with the file
        filename: The filename to retrieve
        prefix: Optional subfolder within the task_id/project
        user: The authenticated user (injected by dependency)
    
    Returns:
        The file content as a response
    """
    try:
        # Get file from S3
        s3_service = S3Service()
        result = s3_service.download_file(
            project_name=task_id,
            file_name=filename,
            prefix=prefix
        )
        
        # Get content and content type from the result
        content = result["content"]
        content_type = result["content_type"]
        
        # Return the file content
        return Response(content=content, media_type=content_type)
    except Exception as e:
        logger.error(f"Failed to retrieve file {filename} from project {task_id}: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=404, detail=f"File not found or error retrieving: {str(e)}")

@router.delete("/content/file/{task_id}/{filename:path}")
async def delete_file(task_id: str, filename: str, prefix: str = None, user: dict = Depends(get_current_token)):
    """
    Deletes a file from S3 storage.
    
    Args:
        task_id: The task ID/project name associated with the file
        filename: The filename to delete
        prefix: Optional subfolder within the task_id/project
        user: The authenticated user (injected by dependency)
    
    Returns:
        JSON with deletion status
    """
    try:
        # Delete file from S3
        s3_service = S3Service()
        result = s3_service.delete_file(
            project_name=task_id,
            file_name=filename,
            prefix=prefix
        )
        
        # Return success response
        return {
            "key": result["key"],
            "filename": result["name"],
            "task_id": task_id,
            "message": "File deleted successfully",
            "status": result["status"]
        }
    except Exception as e:
        logger.error(f"Failed to delete file {filename} from project {task_id}: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=500, detail=f"File deletion failed: {str(e)}")

@router.get("/content/url/{task_id}/{filename:path}")
async def get_presigned_url(
    task_id: str, 
    filename: str, 
    operation: str = "get_object", 
    expires_in: int = 3600, 
    prefix: str = None, 
    user: dict = Depends(get_current_token)
):
    """
    Generates a presigned URL for direct access to a file in S3 storage.
    
    Args:
        task_id: The task ID/project name associated with the file
        filename: The filename to access
        operation: The S3 operation ('get_object', 'put_object', etc.)
        expires_in: URL expiration time in seconds (default: 1 hour)
        prefix: Optional subfolder within the task_id/project
        user: The authenticated user (injected by dependency)
    
    Returns:
        JSON with the presigned URL
    """
    try:
        # Generate presigned URL
        s3_service = S3Service()
        url = s3_service.get_presigned_url(
            project_name=task_id,
            file_name=filename,
            operation=operation,
            expires_in=expires_in,
            prefix=prefix
        )
        
        # Return the URL
        return {
            "url": url,
            "task_id": task_id,
            "filename": filename,
            "operation": operation,
            "expires_in": expires_in,
            "expires_at": int(time.time() + expires_in),
            "message": f"Generated {operation} URL expiring in {expires_in} seconds"
        }
    except Exception as e:
        logger.error(f"Failed to generate presigned URL for {filename} in project {task_id}: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=500, detail=f"Failed to generate presigned URL: {str(e)}")

class CreateS3KeyRequest(BaseModel):
    """Request model for creating S3 access keys for a project"""
    project_name: str = Field(description="The project name to create keys for")
    read_only: bool = Field(default=False, description="Whether to create read-only keys")

@router.post("/content/keys")
async def create_project_keys(
    request: CreateS3KeyRequest,
    user: dict = Depends(get_api_key)  # Higher security: require API key
):
    """
    Creates access keys for a specific project in S3 storage.
    These keys will have limited permissions to only access files within the project.
    
    Args:
        request: The request model containing project_name and read_only flag
        user: The authenticated admin user (injected by dependency)
    
    Returns:
        JSON with the created access keys
    """
    try:
        # Create project access keys
        s3_service = S3Service()
        key_data = s3_service.create_user_key(
            project_name=request.project_name,
            read_only=request.read_only
        )
        
        # Return the key data (sensitive information - admin access only)
        return {
            **key_data,
            "created_at": int(time.time()),
            "message": f"Created {'read-only' if request.read_only else 'read-write'} keys for project {request.project_name}"
        }
    except Exception as e:
        logger.error(f"Failed to create keys for project {request.project_name}: {str(e)}")
        logger.warning(traceback.format_exc())
        raise HTTPException(status_code=500, detail=f"Failed to create project keys: {str(e)}")
        
# Scheduled tasks endpoints
@router.post("/schedules", response_model=Schedule)
async def create_schedule(request: ScheduleCreate, user: dict = Depends(get_api_key)):
    """Create a new scheduled task."""
    from percolate.api.main import scheduler, run_scheduled_job
    # Persist schedule to database
    record = Schedule(id=str(uuid.uuid1()), userid=request.userid, name=request.name, spec=request.spec, schedule=request.schedule)
    result = p8.repository(Schedule).update_records(record)
    # Determine model instance for scheduling
    scheduled = Schedule(**result[0]) if result else record
    # Schedule the job in memory immediately
    try:
        trigger = CronTrigger.from_crontab(scheduled.schedule)
        scheduler.add_job(
            run_scheduled_job,
            trigger,
            args=[scheduled],
            id=str(scheduled.id)
        )
        logger.info(f"Scheduled new job {scheduled.id}")
    except Exception as e:
        logger.warning(f"Failed to add job to scheduler: {e}")
    return scheduled
   
@router.get("/schedules", response_model=typing.List[Schedule])
async def list_schedules(user: dict = Depends(get_api_key)):
    """List all active (non-disabled) schedules."""
 
    repo = p8.repository(Schedule)
    table = Schedule.get_model_table_name()
    data = repo.execute(f"SELECT * FROM {table} WHERE disabled_at IS NULL")
    return [Schedule(**d) for d in data]
   
@router.delete("/schedules/{schedule_id}", response_model=Schedule)
async def disable_schedule(schedule_id: str, user: dict = Depends(get_api_key)):
    """Disable (soft delete) a schedule by setting its disabled_at timestamp."""
    from percolate.api.main import scheduler
    repo = p8.repository(Schedule)
    table = Schedule.get_model_table_name()
    data = repo.execute(f"SELECT * FROM {table} WHERE id = %s", data=(schedule_id,))
    if not data:
        raise HTTPException(status_code=404, detail="Schedule not found")
    existing = Schedule(**data[0])
    existing.disabled_at = datetime.datetime.utcnow()
    result = p8.repository(Schedule).update_records(existing)
    updated = Schedule(**result[0]) if result else existing
    # Remove from scheduler
    try:
        scheduler.remove_job(str(schedule_id))
        logger.info(f"Removed scheduled job {schedule_id}")
    except Exception:
        logger.warning(f"Job {schedule_id} not found in scheduler or failed to remove")
    return updated