use std::path::Path;
use std::time::Duration;

use crate::api::SunoClient;
use crate::api::types::{
    Clip, CreateAudioUploadRequest, CreateAudioUploadSpec, FinishAudioUploadRequest,
    InitializeAudioClipRequest, SetMetadataRequest,
};
use crate::core::{
    CliError, deadline_after, ensure_poll_timeout, run_before_deadline, sleep_before_deadline,
};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct UploadResult {
    pub upload_id: String,
    pub status_upload_id: Option<String>,
    pub clip_id: Option<String>,
    pub clip: Option<Clip>,
    pub has_vocal: Option<bool>,
    pub status: String,
}

pub struct UploadWorkflowInput<'a> {
    pub file: &'a Path,
    pub upload_type: &'a str,
    pub is_stem_mix: bool,
    pub title: Option<String>,
    pub lyrics: Option<String>,
    pub timeout: Duration,
    pub poll_interval: Duration,
}

pub async fn run(
    client: &SunoClient,
    input: UploadWorkflowInput<'_>,
) -> Result<UploadResult, CliError> {
    ensure_poll_timeout(input.timeout)?;
    let extension = audio_extension(input.file)?;
    let filename = upload_filename(input.file)?;
    let file = tokio::fs::File::open(input.file).await?;
    let metadata = file.metadata().await?;
    if !metadata.is_file() {
        return Err(CliError::Config(format!(
            "upload path is not a regular file: {}",
            input.file.display()
        )));
    }

    let upload = client
        .create_audio_upload(&CreateAudioUploadRequest {
            spec: CreateAudioUploadSpec {
                extension,
                is_stem_mix: input.is_stem_mix,
                upload_type: input.upload_type.to_string(),
            },
        })
        .await?;
    let mut completed_steps = vec!["upload_created"];

    client
        .upload_presigned_audio_file(&upload.url, &upload.fields, &filename, file, metadata.len())
        .await
        .map_err(|error| {
            upload_stage_error(&upload.id, None, &completed_steps, "file_upload", error)
        })?;
    completed_steps.push("file_uploaded");

    client
        .finish_audio_upload(
            &upload.id,
            &FinishAudioUploadRequest {
                upload_type: input.upload_type.to_string(),
                upload_filename: filename,
            },
        )
        .await
        .map_err(|error| {
            upload_stage_error(&upload.id, None, &completed_steps, "upload_finish", error)
        })?;
    completed_steps.push("upload_finished");

    let status = wait_until_complete(client, &upload.id, input.timeout, input.poll_interval)
        .await
        .map_err(|error| {
            upload_stage_error(&upload.id, None, &completed_steps, "processing_wait", error)
        })?;
    completed_steps.push("processing_complete");
    let initialized = client
        .initialize_audio_clip(
            &upload.id,
            &InitializeAudioClipRequest {
                downbeats: None,
                user_reviewed_tags: Some(true),
            },
        )
        .await
        .map_err(|error| {
            upload_stage_error(&upload.id, None, &completed_steps, "clip_initialize", error)
        })?;
    completed_steps.push("clip_initialized");

    let clip_id = initialized_clip_id(&initialized).map_err(|error| {
        upload_stage_error(&upload.id, None, &completed_steps, "clip_identity", error)
    })?;

    let title = input.title.or_else(|| status.title.clone());
    let lyrics = input.lyrics;
    let image_url = status.image_url.clone();
    let expected_metadata = ExpectedClipMetadata {
        title: title.clone(),
        lyrics: lyrics.clone(),
        image_url: image_url.clone(),
    };
    let updates_metadata = title.is_some() || lyrics.is_some() || image_url.is_some();
    if updates_metadata {
        client
            .set_metadata(
                &clip_id,
                &SetMetadataRequest {
                    title,
                    lyrics,
                    caption: None,
                    image_url,
                    is_audio_upload_tos_accepted: Some(true),
                    remove_image_cover: None,
                    remove_video_cover: None,
                },
            )
            .await
            .map_err(|error| {
                upload_metadata_stage_error(
                    &upload.id,
                    &clip_id,
                    &completed_steps,
                    &expected_metadata,
                    error,
                )
            })?;
        completed_steps.push("metadata_updated");
    }

    let clip = if updates_metadata {
        Some(
            wait_for_metadata_readback(
                client,
                &clip_id,
                &expected_metadata,
                input.timeout,
                input.poll_interval,
            )
            .await
            .map_err(|error| {
                upload_stage_error(
                    &upload.id,
                    Some(&clip_id),
                    &completed_steps,
                    "readback",
                    error,
                )
            })?,
        )
    } else {
        initialized.clip
    };

    Ok(UploadResult {
        upload_id: upload.id,
        status_upload_id: status.id,
        clip_id: Some(clip_id),
        clip,
        has_vocal: status.has_vocal,
        status: "complete".into(),
    })
}

struct ExpectedClipMetadata {
    title: Option<String>,
    lyrics: Option<String>,
    image_url: Option<String>,
}

impl ExpectedClipMetadata {
    fn matches(&self, clip: &Clip) -> bool {
        self.title.as_ref().is_none_or(|title| clip.title == *title)
            && self
                .lyrics
                .as_ref()
                .is_none_or(|lyrics| clip.metadata.prompt.as_ref() == Some(lyrics))
            && self
                .image_url
                .as_ref()
                .is_none_or(|image_url| clip.image_url.as_ref() == Some(image_url))
    }
}

async fn wait_for_metadata_readback(
    client: &SunoClient,
    clip_id: &str,
    expected: &ExpectedClipMetadata,
    timeout: Duration,
    poll_interval: Duration,
) -> Result<Clip, CliError> {
    let deadline = deadline_after(timeout)?;
    let poll_interval = poll_interval.max(Duration::from_millis(1));
    let requested = [clip_id.to_string()];
    loop {
        let clips = run_before_deadline(
            deadline,
            client.get_clips(&requested),
            metadata_readback_timeout(clip_id, timeout),
        )
        .await?;
        if let Some(clip) = clips.into_iter().find(|clip| clip.id == clip_id)
            && expected.matches(&clip)
        {
            return Ok(clip);
        }
        if !sleep_before_deadline(deadline, poll_interval).await {
            return Err(metadata_readback_timeout(clip_id, timeout));
        }
    }
}

fn metadata_readback_timeout(clip_id: &str, timeout: Duration) -> CliError {
    CliError::GenerationFailed(format!(
        "audio upload metadata for clip {clip_id} did not become visible within {} seconds",
        timeout.as_secs()
    ))
}

fn upload_stage_error(
    upload_id: &str,
    clip_id: Option<&str>,
    completed_steps: &[&str],
    failed_step: &str,
    error: CliError,
) -> CliError {
    let mut details = serde_json::json!({
        "operation": "audio_upload",
        "upload_id": upload_id,
        "completed_steps": completed_steps,
        "failed": {
            "step": failed_step,
            "code": error.error_code(),
            "message": error.to_string()
        }
    });
    if let Some(clip_id) = clip_id {
        details["clip_id"] = serde_json::Value::String(clip_id.to_string());
    }
    details["recovery"] = upload_recovery_details(upload_id, clip_id, failed_step);
    CliError::PartialMutation {
        message: format!(
            "audio upload {upload_id} stopped at {failed_step} after {} completed step(s)",
            completed_steps.len()
        ),
        details,
    }
}

fn upload_metadata_stage_error(
    upload_id: &str,
    clip_id: &str,
    completed_steps: &[&str],
    expected: &ExpectedClipMetadata,
    error: CliError,
) -> CliError {
    let mut error = upload_stage_error(
        upload_id,
        Some(clip_id),
        completed_steps,
        "metadata_update",
        error,
    );
    if let CliError::PartialMutation { details, .. } = &mut error {
        let mut arguments = serde_json::json!({ "clip_id": clip_id });
        if let Some(title) = &expected.title {
            arguments["title"] = serde_json::Value::String(title.clone());
        }
        if let Some(lyrics) = &expected.lyrics {
            arguments["lyrics"] = serde_json::Value::String(lyrics.clone());
        }
        if let Some(image_url) = &expected.image_url {
            arguments["image_url"] = serde_json::Value::String(image_url.clone());
        }
        details["recovery"] = serde_json::json!({
            "resumable": true,
            "command": "sunox clip set",
            "arguments": arguments
        });
    }
    error
}

fn upload_recovery_details(
    upload_id: &str,
    clip_id: Option<&str>,
    failed_step: &str,
) -> serde_json::Value {
    match (failed_step, clip_id) {
        ("processing_wait", _) => serde_json::json!({
            "resumable": false,
            "reason": "clip initialization cannot be safely replayed",
            "inspection": {
                "command": "sunox clip upload-status",
                "arguments": { "upload_id": upload_id }
            }
        }),
        ("metadata_update", _) => serde_json::json!({
            "resumable": false,
            "reason": "metadata recovery requires the exact requested fields"
        }),
        ("readback", Some(clip_id)) => serde_json::json!({
            "resumable": true,
            "command": "sunox clip info",
            "arguments": { "clip_id": clip_id }
        }),
        ("file_upload", _) => serde_json::json!({
            "resumable": false,
            "reason": "the presigned upload form cannot be safely reconstructed"
        }),
        ("upload_finish", _) => serde_json::json!({
            "resumable": false,
            "reason": "retry safety for audio upload finish is not live-verified"
        }),
        ("clip_initialize" | "clip_identity", _) => serde_json::json!({
            "resumable": false,
            "reason": "retry safety for clip initialization is not live-verified"
        }),
        _ => serde_json::json!({ "resumable": false }),
    }
}

pub fn audio_extension(path: &Path) -> Result<String, CliError> {
    let extension = path
        .extension()
        .and_then(|extension| extension.to_str())
        .map(|extension| extension.trim_start_matches('.').to_ascii_lowercase())
        .filter(|extension| !extension.is_empty())
        .ok_or_else(|| CliError::Config("upload file must have an audio extension".into()))?;
    Ok(extension)
}

pub fn upload_filename(path: &Path) -> Result<String, CliError> {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(str::to_string)
        .filter(|name| !name.is_empty())
        .ok_or_else(|| CliError::Config("upload file must have a valid filename".into()))
}

fn initialized_clip_id(
    initialized: &crate::api::types::InitializeAudioClipResponse,
) -> Result<String, CliError> {
    initialized
        .clip_id
        .clone()
        .or_else(|| initialized.clip.as_ref().map(|clip| clip.id.clone()))
        .ok_or_else(|| CliError::Api {
            code: "schema_drift",
            message: "audio upload initialization completed without a clip id".into(),
        })
}

async fn wait_until_complete(
    client: &SunoClient,
    upload_id: &str,
    timeout: Duration,
    poll_interval: Duration,
) -> Result<crate::api::types::AudioUploadStatus, CliError> {
    let deadline = deadline_after(timeout)?;
    let poll_interval = poll_interval.max(Duration::from_secs(1));
    loop {
        let timeout_error = || {
            CliError::GenerationFailed(format!(
                "audio upload {upload_id} did not complete within {} seconds",
                timeout.as_secs()
            ))
        };
        let status = run_before_deadline(
            deadline,
            client.get_audio_upload(upload_id),
            timeout_error(),
        )
        .await?;
        match status.status.as_deref() {
            Some("complete") => return Ok(status),
            Some("error") => {
                return Err(CliError::GenerationFailed(format!(
                    "audio upload {upload_id} failed during processing"
                )));
            }
            _ if !sleep_before_deadline(deadline, poll_interval).await => {
                return Err(timeout_error());
            }
            _ => {}
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;
    use std::time::Duration;

    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpListener;
    use tokio::time::timeout;

    use crate::api::SunoClient;
    use crate::api::types::InitializeAudioClipResponse;
    use crate::auth::AuthState;
    use crate::core::CliError;

    use super::{
        ExpectedClipMetadata, UploadWorkflowInput, audio_extension, initialized_clip_id, run,
        upload_filename, upload_metadata_stage_error, upload_stage_error, wait_until_complete,
    };

    #[test]
    fn audio_extension_lowercases_file_extension() {
        assert_eq!(
            audio_extension(Path::new("/tmp/Demo.MP3")).expect("extension"),
            "mp3"
        );
    }

    #[test]
    fn upload_filename_uses_basename() {
        assert_eq!(
            upload_filename(Path::new("/tmp/Demo.MP3")).expect("filename"),
            "Demo.MP3"
        );
    }

    #[test]
    fn initialized_clip_id_rejects_missing_clip_identity() {
        let response = InitializeAudioClipResponse {
            clip_id: None,
            clip: None,
        };

        let err = initialized_clip_id(&response).expect_err("missing clip id");

        assert_eq!(err.error_code(), "schema_drift");
    }

    #[test]
    fn upload_stage_failure_exposes_recovery_checkpoint() {
        let error = upload_metadata_stage_error(
            "upload-1",
            "clip-1",
            &["upload_created", "file_uploaded", "processing_complete"],
            &ExpectedClipMetadata {
                title: Some("Final title".into()),
                lyrics: Some("Final lyrics".into()),
                image_url: Some("https://cdn.example/cover.jpeg".into()),
            },
            CliError::RateLimited,
        );

        assert_eq!(error.error_code(), "partial_mutation");
        assert_eq!(
            error.details().expect("upload checkpoint details"),
            &serde_json::json!({
                "operation": "audio_upload",
                "upload_id": "upload-1",
                "clip_id": "clip-1",
                "completed_steps": ["upload_created", "file_uploaded", "processing_complete"],
                "failed": {
                    "step": "metadata_update",
                    "code": "rate_limited",
                    "message": "Rate limited by Suno — wait and retry"
                },
                "recovery": {
                    "resumable": true,
                    "command": "sunox clip set",
                    "arguments": {
                        "clip_id": "clip-1",
                        "title": "Final title",
                        "lyrics": "Final lyrics",
                        "image_url": "https://cdn.example/cover.jpeg"
                    }
                }
            })
        );
    }

    #[test]
    fn processing_wait_failure_exposes_read_only_inspection_without_claiming_resume() {
        let error = upload_stage_error(
            "upload-1",
            None,
            &["upload_created", "file_uploaded", "upload_finished"],
            "processing_wait",
            CliError::GenerationFailed("timed out".into()),
        );

        let recovery = &error.details().expect("upload recovery")["recovery"];
        assert_eq!(recovery["resumable"], false);
        assert_eq!(
            recovery["reason"],
            "clip initialization cannot be safely replayed"
        );
        assert_eq!(
            recovery["inspection"]["command"],
            "sunox clip upload-status"
        );
        assert_eq!(recovery["inspection"]["arguments"]["upload_id"], "upload-1");
    }

    #[tokio::test]
    async fn upload_workflow_rejects_zero_timeout_before_file_io() {
        let client = SunoClient::new_for_tests(
            "http://127.0.0.1:9".into(),
            AuthState {
                jwt: Some("test-jwt".into()),
                ..AuthState::default()
            },
        )
        .expect("test client");

        let error = run(
            &client,
            UploadWorkflowInput {
                file: Path::new("missing.wav"),
                upload_type: "file_upload",
                is_stem_mix: false,
                title: None,
                lyrics: None,
                timeout: Duration::ZERO,
                poll_interval: Duration::from_secs(1),
            },
        )
        .await
        .expect_err("zero timeout must be rejected before reading the file");

        assert!(
            matches!(error, CliError::Config(message) if message.contains("poll timeout") && message.contains("greater than 0"))
        );
    }

    #[tokio::test]
    async fn upload_poll_deadline_bounds_an_in_flight_request() {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind upload status server");
        let address = listener.local_addr().expect("upload status address");
        tokio::spawn(async move {
            let (mut stream, _) = listener.accept().await.expect("accept upload poll");
            let mut request = [0_u8; 1024];
            let _ = stream.read(&mut request).await;
            tokio::time::sleep(Duration::from_millis(200)).await;
            let body = r#"{"id":"upload-1","status":"pending"}"#;
            let response = format!(
                "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
                body.len()
            );
            let _ = stream.write_all(response.as_bytes()).await;
        });
        let client = SunoClient::new_for_tests(
            format!("http://{address}"),
            AuthState {
                jwt: Some("test-jwt".into()),
                ..AuthState::default()
            },
        )
        .expect("test client");

        let error = timeout(
            Duration::from_millis(50),
            wait_until_complete(
                &client,
                "upload-1",
                Duration::from_millis(10),
                Duration::from_millis(1),
            ),
        )
        .await
        .expect("upload polling deadline must bound the request")
        .expect_err("delayed upload status must time out");

        assert!(
            matches!(error, CliError::GenerationFailed(message) if message.contains("did not complete"))
        );
    }
}
