from datetime import datetime
import logging

from fastapi import Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session, joinedload

from jockey import (
    TaskType,
    BaseEvent,
    queue_task,
    get_task_status,
    get_task_events,
    get_tasks,
    create_secret,
    delete_secret,
    list_secrets,
    delete_deployment_resources,
)
from jockey.backend.models.image import Image

from agentops.common.orm import get_orm_session
from agentops.common.route_config import BaseView
from agentops.opsboard.models import ProjectModel
from agentops.deploy.models import HostingProjectModel
from agentops.deploy.schemas import (
    StatusResponse,
    HostingProjectResponse,
    UpdateDeploymentRequest,
    DeploymentJobSchema,
    RunJobRequest,
    DeploymentStatusResponse,
    DeploymentEventSchema,
    DeploymentEventResponse,
    DeploymentHistoryResponse,
    SecretSchema,
    CreateSecretRequest,
    ListSecretsResponse,
)

logger = logging.getLogger(__name__)

# project_id is the internal project ID that the deployment belongs to
#    it is the same on both the `ProjectModel` and `HostingProjectModel`
# job_id is the ID generated by the deploy backend for the iteration of the deployment


class BaseDeploymentView(BaseView):
    async def get_hosting_project(self, orm: Session, project_id: str) -> HostingProjectModel:
        # load project in a separate query because ProjectModel.get_by_id loads a
        # bunch of stuff we need for the auth check
        if not (project := ProjectModel.get_by_id(orm, project_id)):
            raise HTTPException(status_code=404, detail="Project not found")

        if not project.org.is_user_member(self.request.state.session.user_id):
            raise HTTPException(status_code=404, detail="Project not found")

        return HostingProjectModel.get_or_create_by_id(orm, project_id)


class CreateUpdateSecretView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        body: CreateSecretRequest,
        orm: Session = Depends(get_orm_session),
    ) -> StatusResponse:
        """
        Create or update a secret for a deployment.

        This will be stored directly on the k8s cluster.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)
        delete_secret(project.namespace, project.id, body.name)
        create_secret(project.namespace, project.id, body.name, body.value)

        return StatusResponse(success=True, message="Successfully created secret")


class ListSecretsView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> ListSecretsResponse:
        """
        List all secrets for a deployment

        Currently, we just list the names of the keys and doesn't allow retrieval of values for security.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)
        secret_names: list[str] = list_secrets(project.namespace, project.id)

        return ListSecretsResponse(
            secrets=[SecretSchema(name=name) for name in secret_names],
        )


class DeleteSecretView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        secret_name: str,
        orm: Session = Depends(get_orm_session),
    ) -> StatusResponse:
        """
        Delete a secret for a deployment.

        This will remove the secret from the k8s cluster.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)
        delete_secret(project.namespace, project.id, secret_name)

        return StatusResponse(success=True, message="Successfully deleted secret")


class UpdateDeploymentView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        body: UpdateDeploymentRequest,
        orm: Session = Depends(get_orm_session),
    ) -> StatusResponse:
        """
        Update deployment configuration for a project.

        Updates the HostingProjectModel with the provided values.
        Only non-None values in the request will be updated.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        for field_name, field_value in body.model_dump(exclude_none=True).items():
            if hasattr(project, field_name):
                setattr(project, field_name, field_value)

        orm.commit()

        return StatusResponse(
            success=True,
            message="Deployment configuration updated successfully",
        )


class InitiateBuildView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> DeploymentStatusResponse:
        """
        Build image only without deploying.

        This endpoint builds the Docker image and pushes it to the registry
        but does not create any Kubernetes deployment resources.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        # Queue the build job for background processing
        job_id = queue_task(
            TaskType.BUILD,
            config=project.deployment_config,
            project_id=project.id,
        )

        return DeploymentStatusResponse(
            success=True,
            message="Image build queued successfully",
            job_id=job_id,
        )


class InitiateDeploymentView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> DeploymentStatusResponse:
        """
        Initiate a deployment for a project.

        Takes config from the `HostingProjectModel` and queues a deployment job.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        job_id = queue_task(
            TaskType.SERVE,
            config=project.deployment_config,
            project_id=project.id,
        )

        return DeploymentStatusResponse(
            success=True,
            message="Deployment initiated successfully",
            job_id=job_id,
        )


class InitiateRunView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        body: RunJobRequest,
        orm: Session = Depends(get_orm_session),
    ) -> DeploymentStatusResponse:
        """
        Build image and run as a one-time job with input data.

        This endpoint builds the image (if needed) and then runs it as a
        Kubernetes Job that executes once with the provided input data
        and returns the result.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        # Queue the job run for background processing
        job_id = queue_task(
            TaskType.RUN,
            config=project.deployment_config,
            project_id=project.id,
            inputs=body.inputs,
            callback_url=body.callback_url,
        )

        return DeploymentStatusResponse(
            success=True,
            job_id=job_id,
            message="Job execution queued successfully",
        )


class DeploymentStatusView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        job_id: str,
        start_date: datetime | None = None,
        orm: Session = Depends(get_orm_session),
    ) -> DeploymentEventResponse:
        """
        Get a list of events from a specific deployment job.

        If `start_date` is provided, only events after that date will be returned.
        This is useful for polling new events since the last check.
        If no `start_date` is provided, all events will be returned.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)
        events: list[BaseEvent] = get_task_events(
            job_id=job_id,
            start_time=start_date,
        )

        return DeploymentEventResponse(
            events=[
                DeploymentEventSchema(
                    type=event.event_type,
                    status=event.status,
                    message=event.message,
                    timestamp=event.timestamp,
                )
                for event in events
            ],
        )


class DeploymentBuildLogView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        job_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> StreamingResponse:
        """
        Stream build logs from the builder pod for a specific deployment job.

        Args:
            project_id: The project ID
            job_id: The deployment job ID
            orm: Database session

        Returns:
            StreamingResponse with plain text logs
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        # Use job_id to find the builder pod (builder pods are now named with job_id)
        if not (pod := Image.get_builder_pod(project.namespace, job_id)):
            raise HTTPException(status_code=404, detail=f"Builder pod not found for job {job_id}")

        # TODO this is not flushing mid-stream for some reason.
        return StreamingResponse(
            pod.stream_logs(project.namespace),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache, no-store, must-revalidate",
                "Pragma": "no-cache",
                "Expires": "0",
                "Connection": "keep-alive",
                # "X-Accel-Buffering": "no",
                # "X-Content-Type-Options": "nosniff",
            },
        )


class DeploymentHistoryView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> DeploymentHistoryResponse:
        """
        Get a list of deployments and their last status event for a specific project.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        jobs = []
        for job in get_tasks(project.namespace, project.id):
            status_event: BaseEvent = get_task_status(job["job_id"])

            try:
                status = status_event.status.value
                message = status_event.message
            except (IndexError, AttributeError):
                status = "unknown"
                message = ""

            jobs.append(
                DeploymentJobSchema(
                    id=job["job_id"],
                    queued_at=job["queued_at"],
                    status=status,
                    message=message,
                )
            )

        return DeploymentHistoryResponse(jobs=jobs)


class ListUserDeploymentsView(BaseDeploymentView):
    async def __call__(
        self,
        orm: Session = Depends(get_orm_session),
    ) -> list[HostingProjectResponse]:
        """
        Get all projects for the current user that have a deployment (HostingProjectModel).
        Returns combined data from both ProjectModel and HostingProjectModel.
        """
        user_id = self.request.state.session.user_id

        # Get all projects the user has access to
        projects = ProjectModel.get_all_for_user(orm, user_id)
        project_ids = [p.id for p in projects]

        if not project_ids:
            return []

        # Query HostingProjectModel for all records with a matching project id
        hosting_projects = (
            orm.query(HostingProjectModel)
            .filter(HostingProjectModel.id.in_(project_ids))
            .options(joinedload(HostingProjectModel.project).joinedload(ProjectModel.org))
            .all()
        )

        # Create a mapping of project_id to hosting_project for easy lookup
        hosting_project_map = {hp.id: hp for hp in hosting_projects}

        # Combine data from both models
        combined_projects = []
        for project in projects:
            if project.id in hosting_project_map:
                hosting_project = hosting_project_map[project.id]

                # Create combined response
                combined_project = HostingProjectResponse(
                    # Project fields
                    id=project.id,
                    name=project.name,
                    api_key=project.api_key,
                    org_id=project.org_id,
                    environment=project.environment,
                    org=project.org,
                    trace_count=0,  # TODO: Add actual trace count if needed
                    # Hosting project fields
                    git_url=hosting_project.git_url,
                    git_branch=hosting_project.git_branch,
                    entrypoint=hosting_project.entrypoint,
                    watch_path=hosting_project.watch_path,
                    user_callback_url=hosting_project.user_callback_url,
                    github_oath_access_token=hosting_project.github_oath_access_token,
                )

                combined_projects.append(combined_project)

        return combined_projects


class DeleteDeploymentView(BaseDeploymentView):
    async def __call__(
        self,
        project_id: str,
        orm: Session = Depends(get_orm_session),
    ) -> StatusResponse:
        """
        Delete a deployment for a project.

        This will remove the HostingProjectModel record and clean up any associated resources.
        """
        project: HostingProjectModel = await self.get_hosting_project(orm, project_id)

        # Delete all Kubernetes resources associated with the deployment
        # This includes deployments, services, ingress, and secrets
        deployment_name = project_id  # Use project_id as deployment name
        namespace = project.namespace

        try:
            success = delete_deployment_resources(
                namespace=namespace, deployment_name=deployment_name, deployment_id=project_id
            )

            if not success:
                logger.warning(f"Some Kubernetes resources failed to delete for project {project_id}")
                # Continue with database deletion even if k8s cleanup had issues
        except Exception as e:
            logger.error(f"Error during Kubernetes cleanup for project {project_id}: {e}")
            # Continue with database deletion even if k8s cleanup failed

        # Delete the HostingProjectModel record
        orm.delete(project)
        orm.commit()

        return StatusResponse(
            success=True,
            message="Deployment deleted successfully",
        )
