use crate::AppService;
use crate::error::AppError;
use chrono::Utc;
use models::issues::{
    IssueState, issue, issue_assignee, issue_comment, issue_label, issue_repo, issue_subscriber,
};
use models::projects::project_members;
use models::users::user;
use redis::AsyncCommands;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct IssueCreateRequest {
    pub title: String,
    pub body: Option<String>,
    pub milestone: Option<String>,
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct IssueUpdateRequest {
    pub title: Option<String>,
    pub body: Option<String>,
    pub milestone: Option<String>,
    pub state: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct IssueResponse {
    pub id: Uuid,
    pub project: Uuid,
    pub number: i64,
    pub title: String,
    pub body: Option<String>,
    pub state: String,
    pub author: Uuid,
    pub author_username: Option<String>,
    pub milestone: Option<String>,
    pub created_at: chrono::DateTime<Utc>,
    pub updated_at: chrono::DateTime<Utc>,
    pub closed_at: Option<chrono::DateTime<Utc>>,
    pub created_by_ai: bool,
}

impl From<issue::Model> for IssueResponse {
    fn from(i: issue::Model) -> Self {
        Self {
            id: i.id,
            project: i.project,
            number: i.number,
            title: i.title,
            body: i.body,
            state: i.state,
            author: i.author,
            author_username: None,
            milestone: i.milestone,
            created_at: i.created_at,
            updated_at: i.updated_at,
            closed_at: i.closed_at,
            created_by_ai: i.created_by_ai,
        }
    }
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssueListResponse {
    pub issues: Vec<IssueResponse>,
    pub total: i64,
    pub page: i64,
    pub per_page: i64,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssueSummaryResponse {
    pub total: i64,
    pub open: i64,
    pub closed: i64,
}

impl AppService {
    /// List issues for a project with optional state filter.
    pub async fn issue_list(
        &self,
        project_name: String,
        state: Option<String>,
        page: Option<i64>,
        per_page: Option<i64>,
        ctx: &Session,
    ) -> Result<IssueListResponse, AppError> {
        let project = self.utils_find_project_by_name(project_name).await?;

        // Check membership for private projects
        if let Some(uid) = ctx.user() {
            self.check_project_access(project.id, uid).await?;
        }

        let page = page.unwrap_or(1);
        let per_page = per_page.unwrap_or(20);
        let offset = (page - 1) * per_page;

        let mut query = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .order_by_desc(issue::Column::CreatedAt);

        if let Some(ref s) = state {
            query = query.filter(issue::Column::State.eq(s));
        }

        let total = query.clone().count(&self.db).await?;
        let issues = query
            .offset(offset as u64)
            .limit(per_page as u64)
            .all(&self.db)
            .await?;

        let author_ids: Vec<Uuid> = issues.iter().map(|i| i.author).collect();
        let authors = if author_ids.is_empty() {
            vec![]
        } else {
            user::Entity::find()
                .filter(user::Column::Uid.is_in(author_ids))
                .all(&self.db)
                .await?
        };

        let responses: Vec<IssueResponse> = issues
            .into_iter()
            .map(|i| {
                let username = authors
                    .iter()
                    .find(|u| u.uid == i.author)
                    .map(|u| u.username.clone());
                IssueResponse {
                    author_username: username,
                    ..IssueResponse::from(i)
                }
            })
            .collect();

        Ok(IssueListResponse {
            issues: responses,
            total: total as i64,
            page,
            per_page,
        })
    }

    /// Get a single issue by project + number.
    pub async fn issue_get(
        &self,
        project_name: String,
        number: i64,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        let project = self.utils_find_project_by_name(project_name).await?;
        if let Some(uid) = ctx.user() {
            self.check_project_access(project.id, uid).await?;
        }

        let cache_key = format!("issue:get:{}:{}", project.id, number);
        if let Ok(mut conn) = self.cache.conn().await {
            if let Ok(cached) = conn.get::<_, String>(cache_key.clone()).await {
                if let Ok(cached) = serde_json::from_str::<IssueResponse>(&cached) {
                    return Ok(cached);
                }
            }
        }

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        let author = user::Entity::find_by_id(issue.author)
            .one(&self.db)
            .await
            .ok()
            .flatten();
        let username = author.map(|u| u.username);

        let response = IssueResponse {
            author_username: username,
            ..IssueResponse::from(issue)
        };

        if let Ok(mut conn) = self.cache.conn().await {
            let _: Option<()> = conn
                .set_ex::<String, String, ()>(
                    cache_key,
                    serde_json::to_string(&response).unwrap_or_default(),
                    300,
                )
                .await
                .ok();
        }

        Ok(response)
    }

    /// Get the next sequential issue number for a project.
    async fn next_issue_number(&self, project_id: Uuid) -> Result<i64, AppError> {
        let max_num: Option<Option<i64>> = issue::Entity::find()
            .filter(issue::Column::Project.eq(project_id))
            .select_only()
            .column_as(issue::Column::Number.max(), "max_num")
            .into_tuple::<Option<i64>>()
            .one(&self.db)
            .await?;
        Ok(max_num.flatten().unwrap_or(0) + 1)
    }

    /// Create a new issue.
    pub async fn issue_create(
        &self,
        project_name: String,
        request: IssueCreateRequest,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name.clone()).await?;

        // Any project member can create issues
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project.id))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?;
        if member.is_none() {
            return Err(AppError::NoPower);
        }

        let number = self.next_issue_number(project.id).await?;
        let now = Utc::now();

        let active = issue::ActiveModel {
            id: Set(Uuid::now_v7()),
            project: Set(project.id),
            number: Set(number),
            title: Set(request.title),
            body: Set(request.body),
            state: Set(IssueState::Open.to_string()),
            author: Set(user_uid),
            milestone: Set(request.milestone),
            created_at: Set(now),
            updated_at: Set(now),
            closed_at: Set(None),
            created_by_ai: Set(false),
            ..Default::default()
        };
        let model = active.insert(&self.db).await?;

        // Log activity
        let actor_username = user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username)
            .unwrap_or_default();
        let _ = self
            .project_log_activity(
                project.id,
                None,
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "issue_open".to_string(),
                    title: format!("{} opened issue #{}", actor_username, number),
                    repo_id: None,
                    content: Some(model.title.clone()),
                    event_id: Some(model.id),
                    event_sub_id: Some(model.number),
                    metadata: None,
                    is_private: false,
                },
            )
            .await;

        // Run AI triage asynchronously
        let project_name_clone = project_name.clone();
        let issue_number = number;
        let this = self.clone();
        drop(project_name); // allow move below
        tokio::spawn(async move {
            let _ = this.triage_issue(project_name_clone, issue_number).await;
        });

        Ok(IssueResponse::from(model))
    }

    /// Update an issue (title, body, milestone).
    pub async fn issue_update(
        &self,
        project_name: String,
        number: i64,
        request: IssueUpdateRequest,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project.id))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?;
        if member.is_none() {
            return Err(AppError::NoPower);
        }

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        let mut active: issue::ActiveModel = issue.clone().into();
        if let Some(title) = request.title {
            active.title = Set(title);
        }
        if let Some(body) = request.body {
            active.body = Set(Some(body));
        }
        if let Some(milestone) = request.milestone {
            active.milestone = Set(Some(milestone));
        }
        active.updated_at = Set(Utc::now());

        let model = active.update(&self.db).await?;

        self.invalidate_issue_cache(project.id, number).await;

        let actor_username = user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username)
            .unwrap_or_default();
        let _ = self
            .project_log_activity(
                project.id,
                None,
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "issue_update".to_string(),
                    title: format!("{} updated issue #{}", actor_username, number),
                    repo_id: None,
                    content: Some(model.title.clone()),
                    event_id: Some(model.id),
                    event_sub_id: Some(model.number),
                    metadata: None,
                    is_private: false,
                },
            )
            .await;

        Ok(IssueResponse::from(model))
    }

    /// Close an issue.
    pub async fn issue_close(
        &self,
        project_name: String,
        number: i64,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        self.issue_set_state(project_name, number, IssueState::Closed, ctx)
            .await
    }

    /// Reopen a closed issue.
    pub async fn issue_reopen(
        &self,
        project_name: String,
        number: i64,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        self.issue_set_state(project_name, number, IssueState::Open, ctx)
            .await
    }

    async fn issue_set_state(
        &self,
        project_name: String,
        number: i64,
        state: IssueState,
        ctx: &Session,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project.id))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?;
        if member.is_none() {
            return Err(AppError::NoPower);
        }

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        let now = Utc::now();
        let closed_at = if state == IssueState::Closed {
            Some(now)
        } else {
            None
        };

        let mut active: issue::ActiveModel = issue.clone().into();
        active.state = Set(state.to_string());
        active.updated_at = Set(now);
        active.closed_at = Set(closed_at);
        let model = active.update(&self.db).await?;

        self.invalidate_issue_cache(project.id, number).await;

        let actor_username = user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username)
            .unwrap_or_default();
        let event_type = if state == IssueState::Closed {
            "issue_close"
        } else {
            "issue_reopen"
        };
        let _ = self
            .project_log_activity(
                project.id,
                None,
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: event_type.to_string(),
                    title: format!(
                        "{} {} issue #{}",
                        actor_username,
                        if state == IssueState::Closed {
                            "closed"
                        } else {
                            "reopened"
                        },
                        number
                    ),
                    repo_id: None,
                    content: Some(model.title.clone()),
                    event_id: Some(model.id),
                    event_sub_id: Some(model.number),
                    metadata: None,
                    is_private: false,
                },
            )
            .await;

        Ok(IssueResponse::from(model))
    }

    /// Delete an issue. Only author or admin/owner can delete.
    pub async fn issue_delete(
        &self,
        project_name: String,
        number: i64,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        // Allow if user is author OR admin/owner
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project.id))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?
            .ok_or(AppError::NoPower)?;

        let role = member.scope_role().map_err(|_| AppError::RoleParseError)?;
        let is_author = issue.author == user_uid;
        let is_admin = role == models::projects::MemberRole::Admin
            || role == models::projects::MemberRole::Owner;

        if !is_author && !is_admin {
            return Err(AppError::NoPower);
        }

        // Cascade delete related records
        issue_comment::Entity::delete_many()
            .filter(issue_comment::Column::Issue.eq(issue.id))
            .exec(&self.db)
            .await?;
        issue_assignee::Entity::delete_many()
            .filter(issue_assignee::Column::Issue.eq(issue.id))
            .exec(&self.db)
            .await?;
        issue_label::Entity::delete_many()
            .filter(issue_label::Column::Issue.eq(issue.id))
            .exec(&self.db)
            .await?;
        issue_subscriber::Entity::delete_many()
            .filter(issue_subscriber::Column::Issue.eq(issue.id))
            .exec(&self.db)
            .await?;
        issue_repo::Entity::delete_many()
            .filter(issue_repo::Column::Issue.eq(issue.id))
            .exec(&self.db)
            .await?;

        issue::Entity::delete_by_id((issue.id, issue.number))
            .exec(&self.db)
            .await?;

        self.invalidate_issue_cache(project.id, number).await;

        let actor_username = user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username)
            .unwrap_or_default();
        let _ = self
            .project_log_activity(
                project.id,
                None,
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "issue_delete".to_string(),
                    title: format!("{} deleted issue #{}", actor_username, number),
                    repo_id: None,
                    content: None,
                    event_id: None,
                    event_sub_id: Some(number),
                    metadata: None,
                    is_private: false,
                },
            )
            .await;

        Ok(())
    }

    /// Get issue summary (open/closed counts).
    pub async fn issue_summary(
        &self,
        project_name: String,
        ctx: &Session,
    ) -> Result<IssueSummaryResponse, AppError> {
        let project = self.utils_find_project_by_name(project_name).await?;
        if let Some(uid) = ctx.user() {
            self.check_project_access(project.id, uid).await?;
        }

        let total: u64 = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .count(&self.db)
            .await?;
        let open: u64 = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::State.eq(IssueState::Open.to_string()))
            .count(&self.db)
            .await?;
        let closed = total - open;

        Ok(IssueSummaryResponse {
            total: total as i64,
            open: open as i64,
            closed: closed as i64,
        })
    }

    pub(crate) async fn invalidate_issue_cache(&self, project_id: Uuid, number: i64) {
        if let Ok(mut conn) = self.cache.conn().await {
            let key = format!("issue:get:{}:{}", project_id, number);
            let _: Option<()> = conn.del::<_, ()>(key).await.ok();
        }
    }
}
