use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use time::OffsetDateTime;

use crate::lfd::queue::QueueRunView;
use crate::lfd::sessions::types::ContextSnapshot;
use crate::lfd::sessions::usage::{
    StepUsageAggregate, TokenTotals, UsageSummaryGroupAggregate, UsageTimeseriesBucketAggregate,
};
use crate::lfd::types::{
    ActivationLog, AttentionItem, ChatMemoryBlock, ChatMessage, LivePullRequestState,
    TerminalSession, Trigger, WaveCron, WaveRun, WaveRunStatus,
};

#[derive(Debug, Serialize)]
pub struct HealthResponse {
    pub status: String,
    pub uptime_seconds: i64,
    pub database: bool,
    pub waves_running: u32,
    pub agents_active: u32,
}

#[derive(Debug, Serialize)]
pub struct StatusResponse {
    pub pid: u32,
    pub waves_defined: u32,
    pub waves_running: u32,
    pub agents_active: u32,
    pub slots_used: u32,
    pub slots_total: u32,
}

#[derive(Debug, Serialize)]
pub struct MetricsResponse {
    pub waves_total: u32,
    pub waves_running: u32,
    pub agents_active: u32,
    pub slots_used: u32,
    pub slots_total: u32,
}

#[derive(Debug, Serialize)]
pub struct ErrorDetail {
    #[serde(rename = "type")]
    pub error_type: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub param: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: ErrorDetail,
}

#[derive(Debug, Serialize)]
pub struct ListResponse<T> {
    pub object: String,
    pub data: Vec<T>,
    pub has_more: bool,
}

impl<T> ListResponse<T> {
    pub fn new(data: Vec<T>, has_more: bool) -> Self {
        Self {
            object: "list".to_string(),
            data,
            has_more,
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CommitEntryDto {
    pub sha: String,
    pub message: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct WaveDto {
    pub id: String,
    pub object: String,
    pub name: String,
    pub repo: String,
    pub mode: String,
    pub primary_flow: String,
    pub direction: Vec<String>,
    pub area: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub step_agents: Option<HashMap<String, String>>,
    pub created_at: Option<String>,
    pub status: String,
    pub iteration: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub local_worktree: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remote_branch: Option<String>,
    pub commits: Vec<CommitEntryDto>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diff_stat: Option<String>,
    pub flow_steps: Vec<String>,
    pub open_pr_count: u32,
    pub stack_count: u32,
    pub has_stale_pr_state: bool,
    pub workers: u32,
    pub triggers: Vec<TriggerDto>,
    pub crons: Vec<WaveCronDto>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active_run: Option<WaveRunDto>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct WaveRunDto {
    pub id: String,
    pub object: String,
    pub wave_id: String,
    pub flow: String,
    pub repo: String,
    pub direction: Vec<String>,
    pub area: Vec<String>,
    pub iteration: u32,
    pub step_index: u32,
    pub status: String,
    pub local_worktree: String,
    pub remote_branch: String,
    pub target_branch: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pr: Option<PullRequestDto>,
    pub started_at: Option<String>,
    pub ended_at: Option<String>,
    pub error: Option<String>,
    pub flow_parents: Vec<String>,
    pub stack_position: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_run_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_pr_number: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub live_pr_state: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub live_pr_is_draft: Option<bool>,
    pub pr_state_stale: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub queue_role: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub queue_block_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub queue_blocked_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_action: Option<String>,
    pub created_at: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PullRequestDto {
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub number: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct RunWaveResponse {
    pub started: bool,
    pub wave_id: String,
    pub wave_run_id: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TriggerDto {
    pub id: String,
    pub signal: String,
    pub enabled: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub flow: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_wave_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_main_sha: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_triggered_at: Option<i64>,
    pub created_at: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct WaveCronDto {
    pub id: String,
    pub flow: String,
    pub schedule: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_triggered_at: Option<i64>,
    pub created_at: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AttentionItemDto {
    pub id: String,
    pub object: String,
    pub wave_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run_id: Option<String>,
    pub kind: String,
    pub status: String,
    pub title: String,
    pub summary: String,
    pub context: serde_json::Value,
    pub surfaced_at: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub viewed_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved_at: Option<String>,
}

pub fn attention_item_dto(item: AttentionItem) -> AttentionItemDto {
    AttentionItemDto {
        id: item.id.to_string(),
        object: "attention_item".to_string(),
        wave_id: item.wave_id.to_string(),
        run_id: item.run_id.map(|value| value.to_string()),
        kind: item.kind.as_str().to_string(),
        status: item.status.as_str().to_string(),
        title: item.title,
        summary: item.summary,
        context: item.context,
        surfaced_at: format_datetime(Some(item.surfaced_at)).unwrap_or_default(),
        viewed_at: format_datetime(item.viewed_at),
        resolved_at: format_datetime(item.resolved_at),
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TerminalSessionDto {
    pub id: String,
    pub object: String,
    pub wave_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub wave_run_id: Option<String>,
    pub step: String,
    pub agent: String,
    pub cwd: String,
    pub argv: Vec<String>,
    pub env: BTreeMap<String, String>,
    pub source: String,
    pub tmux_name: String,
    pub status: String,
    pub created_at: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attached_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub started_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub completed_at: Option<String>,
}

pub fn terminal_session_dto(session: TerminalSession) -> TerminalSessionDto {
    TerminalSessionDto {
        id: session.id.to_string(),
        object: "terminal_session".to_string(),
        wave_id: session.wave_id.to_string(),
        wave_run_id: session.wave_run_id.map(|value| value.to_string()),
        step: session.step,
        agent: session.agent,
        cwd: session.cwd,
        argv: session.argv,
        env: session.env,
        source: session.source,
        tmux_name: session.tmux_name,
        status: session.status.as_str().to_string(),
        created_at: format_datetime(Some(session.created_at)).unwrap_or_default(),
        attached_at: format_datetime(session.attached_at),
        started_at: format_datetime(session.started_at),
        completed_at: format_datetime(session.completed_at),
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTerminalSessionRequestDto {
    pub wave_id: String,
    pub flow: String,
    pub worktree: String,
    pub agent: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTerminalSessionResponseDto {
    pub session: TerminalSessionDto,
    pub connection: TerminalConnectionInfoDto,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TerminalConnectionInfoDto {
    pub session_name: String,
    pub host: String,
    pub cwd: String,
    pub status: String,
}

pub fn terminal_connection_info_dto(
    session: &TerminalSession,
    host: String,
) -> TerminalConnectionInfoDto {
    TerminalConnectionInfoDto {
        session_name: session.tmux_name.clone(),
        host,
        cwd: session.cwd.clone(),
        status: session.status.as_str().to_string(),
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ActivationLogDto {
    pub id: String,
    pub object: String,
    pub wave_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_id: Option<String>,
    pub reason: String,
    pub outcome: String,
    pub created_at: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ChatMemoryBlockDto {
    pub object: String,
    pub name: String,
    pub content: String,
    pub position: u32,
    pub updated_at: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ChatStartedDto {
    pub object: String,
    pub wave_id: String,
    pub status: String,
}

#[derive(Debug, Serialize)]
pub struct ChatMessageDto {
    pub id: String,
    pub object: String,
    pub role: String,
    pub content: String,
    pub created_at: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct StopWaveResponse {
    pub stopped: bool,
}

#[derive(Debug, Serialize)]
pub struct RestartStepResponse {
    pub restarted: bool,
    pub wave_id: String,
    pub wave_run_id: String,
    pub step_index: u32,
}

#[derive(Debug, Serialize)]
pub struct ContinueWaveResponse {
    pub continued: bool,
    pub wave_id: String,
    pub wave_run_id: String,
}

#[derive(Debug, Serialize)]
pub struct LandWaveResponse {
    pub merged: bool,
}

#[derive(Debug, Serialize)]
pub struct NextWaveResponse {
    pub new_branch: String,
}

#[derive(Debug, Serialize)]
pub struct CombineResponse {
    pub ok: bool,
    pub result: CombineResponseResult,
}

#[derive(Debug, Serialize)]
pub struct CombineResponseResult {
    pub new_pr_url: Option<String>,
    pub closed_prs: Vec<u64>,
}

#[derive(Debug, Serialize)]
pub struct DeletedResourceResponse {
    pub id: String,
    pub object: String,
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct SessionUsageSessionDto {
    pub step: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub wave: Option<String>,
    pub status: String,
    pub created_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ended_at: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct SessionUsageDto {
    pub object: String,
    pub session_id: String,
    pub tokens: TokenTotals,
    pub turns: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context: Option<ContextSnapshot>,
    pub models: BTreeMap<String, u64>,
    pub session: SessionUsageSessionDto,
}

#[derive(Debug, Clone, Serialize)]
pub struct WaveUsageDto {
    pub object: String,
    pub wave_id: String,
    pub tokens: TokenTotals,
    pub sessions: u64,
    pub turns: u64,
    pub models: BTreeMap<String, u64>,
    pub by_step: BTreeMap<String, StepUsageAggregate>,
}

#[derive(Debug, Clone, Serialize)]
pub struct UsageSummaryDto {
    pub object: String,
    pub group_by: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<String>,
    pub groups: Vec<UsageSummaryGroupAggregate>,
}

#[derive(Debug, Clone, Serialize)]
pub struct UsageTimeseriesDto {
    pub object: String,
    pub bucket: String,
    pub group_by: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<String>,
    pub buckets: Vec<UsageTimeseriesBucketAggregate>,
}

pub fn format_datetime(datetime: Option<OffsetDateTime>) -> Option<String> {
    datetime?
        .format(&time::format_description::well_known::Rfc3339)
        .ok()
}

pub fn wave_run_dto(
    run: WaveRun,
    live_pr_state: Option<&LivePullRequestState>,
    pr_state_stale: bool,
    queue_view: Option<&QueueRunView>,
) -> WaveRunDto {
    WaveRunDto {
        id: run.id.to_string(),
        object: "wave_run".to_string(),
        wave_id: run.wave_id.to_string(),
        flow: run.snapshot.flow.clone(),
        repo: run.snapshot.repo.clone(),
        direction: run.snapshot.direction.clone(),
        area: run.snapshot.area.clone(),
        iteration: run.iteration,
        step_index: run.step_index,
        status: wave_run_status_str(run.status),
        local_worktree: run.worktree,
        remote_branch: run.branch,
        target_branch: run.target_branch,
        pr: run.pr.map(|pr| PullRequestDto {
            url: pr.url,
            number: pr.number,
            state: pr.state,
            title: pr.title,
            branch: pr.branch,
        }),
        started_at: format_datetime(run.started_at),
        ended_at: format_datetime(run.ended_at),
        error: run.error,
        flow_parents: run.flow_parents,
        stack_position: run.stack_position,
        parent_run_id: run.parent_run_id.map(|value| value.to_string()),
        parent_pr_number: run.parent_pr_number,
        live_pr_state: live_pr_state.map(|value| value.state.as_str().to_string()),
        live_pr_is_draft: live_pr_state.map(|value| value.is_draft),
        pr_state_stale,
        queue_role: queue_view.map(|value| value.role.as_str().to_string()),
        queue_block_reason: queue_view
            .and_then(|value| value.block_reason.map(|reason| reason.as_str().to_string())),
        queue_blocked_at: format_datetime(queue_view.and_then(|value| value.blocked_at)),
        next_action: queue_view.map(|value| value.next_action.as_str().to_string()),
        created_at: format_datetime(run.started_at),
    }
}

pub fn trigger_dto(t: Trigger) -> TriggerDto {
    TriggerDto {
        id: t.id.to_string(),
        signal: t.signal.as_str().to_string(),
        enabled: t.enabled,
        flow: t.flow,
        source_wave_id: t.source_wave_id.map(|value| value.to_string()),
        last_main_sha: t.last_main_sha,
        last_triggered_at: t.last_triggered_at,
        created_at: format_datetime(t.created_at),
    }
}

pub fn wave_cron_dto(cron: WaveCron) -> WaveCronDto {
    WaveCronDto {
        id: cron.id.to_string(),
        flow: cron.flow,
        schedule: cron.schedule,
        last_triggered_at: cron.last_triggered_at,
        created_at: format_datetime(cron.created_at),
    }
}

pub fn activation_log_dto(log: ActivationLog) -> ActivationLogDto {
    ActivationLogDto {
        id: log.id.to_string(),
        object: "activation_log".to_string(),
        wave_id: log.wave_id.to_string(),
        trigger_id: log.trigger_id.map(|id| id.to_string()),
        reason: log.reason,
        outcome: log.outcome.as_str().to_string(),
        created_at: format_datetime(time::OffsetDateTime::from_unix_timestamp(log.created_at).ok()),
    }
}

pub fn chat_memory_block_dto(block: ChatMemoryBlock) -> ChatMemoryBlockDto {
    ChatMemoryBlockDto {
        object: "memory_block".to_string(),
        name: block.name,
        content: block.content,
        position: block.position,
        updated_at: format_datetime(block.updated_at),
    }
}

pub fn chat_message_dto(message: ChatMessage) -> ChatMessageDto {
    ChatMessageDto {
        id: message.id.to_string(),
        object: "chat_message".to_string(),
        role: message.role,
        content: message.content,
        created_at: format_datetime(Some(message.created_at)),
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct WorktreeDto {
    pub object: String,
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
    pub merged: bool,
    pub prunable: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub wave_id: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct RepoDto {
    pub object: String,
    pub path: String,
    pub name: String,
    pub repo_id: String,
    pub wave_count: u32,
    pub registered: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub added_at: Option<String>,
}

pub fn wave_run_status_str(status: WaveRunStatus) -> String {
    match status {
        WaveRunStatus::Pending => "pending",
        WaveRunStatus::Running => "running",
        WaveRunStatus::Waiting => "waiting",
        WaveRunStatus::Completed => "completed",
        WaveRunStatus::Failed => "failed",
        WaveRunStatus::Unspecified => "unknown",
    }
    .to_string()
}

#[cfg(test)]
mod contract_tests {
    use super::*;

    fn fixture(name: &str) -> String {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../tests/fixtures")
            .join(name);
        std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", path.display()))
    }

    #[test]
    fn wave_fixture_deserializes() {
        let json = fixture("wave.json");
        let wave: WaveDto = serde_json::from_str(&json).unwrap();

        assert_eq!(wave.id, "wave_abc123");
        assert_eq!(wave.object, "wave");
        assert_eq!(wave.name, "engbot");
        assert_eq!(wave.primary_flow, "build");
        assert_eq!(wave.mode, "loop");
        assert_eq!(wave.status, "running");
        assert_eq!(wave.iteration, 3);
        assert_eq!(wave.workers, 1);
        assert_eq!(wave.direction, vec!["ux", "clarity"]);
        assert_eq!(wave.area, vec!["src/"]);
        assert_eq!(wave.open_pr_count, 1);

        assert_eq!(wave.triggers.len(), 2);
        assert_eq!(wave.triggers[0].signal, "repo");
        assert_eq!(wave.triggers[0].flow.as_deref(), Some("integrate"));
        assert_eq!(wave.triggers[1].signal, "ci_failure");
        assert_eq!(wave.crons.len(), 1);
        assert_eq!(wave.crons[0].flow, "wave-polish");

        assert_eq!(wave.commits.len(), 1);
        assert_eq!(wave.commits[0].sha, "abc1234");
    }

    #[test]
    fn trigger_fixture_deserializes() {
        let json = fixture("trigger.json");
        let trigger: TriggerDto = serde_json::from_str(&json).unwrap();

        assert_eq!(trigger.id, "trig_abc123");
        assert_eq!(trigger.signal, "wave");
        assert!(trigger.enabled);
        assert_eq!(trigger.flow.as_deref(), Some("build"));
        assert_eq!(trigger.source_wave_id.as_deref(), Some("wave_upstream"));
        assert_eq!(trigger.last_main_sha.as_deref(), Some("deadbeef1234"));
        assert_eq!(trigger.last_triggered_at, Some(1737000000));
    }

    #[test]
    fn activation_log_fixture_deserializes() {
        let json = fixture("activation_log.json");
        let log: ActivationLogDto = serde_json::from_str(&json).unwrap();

        assert_eq!(log.id, "act_abc123");
        assert_eq!(log.object, "activation_log");
        assert_eq!(log.wave_id, "wave_abc123");
        assert_eq!(log.trigger_id.as_deref(), Some("trig_001"));
        assert_eq!(log.outcome, "started");
        assert!(log.reason.contains("main"));
    }
}
