use std::time::Duration;

use bytes::Bytes;
use dotenvy::dotenv;
use futures_util::future::BoxFuture;
use http::{Request, Response, StatusCode};
use http_body_util::{Empty, Full};
use sunocore::models::aligned_lyrics::AlignedLyricsV3;
use sunocore::models::downbeats::{Downbeats, DownbeatsState};
use sunocore::models::feed::{
    FeedFilters, FeedRequest, FeedResponse, PresenceFilter, WorkspaceFilter,
};
use sunocore::models::instruments::Instruments;
use sunocore::models::key::Key;
use sunocore::models::midi::Midi;
use sunocore::models::studio::StudioProject;
use sunocore::models::waveform::WaveformAggregates;
use sunocore::services::aligned_lyrics::{GetAlignedLyricsV3, GetAlignedLyricsV3Request};
use sunocore::services::downbeats::{GetDownbeats, GetDownbeatsRequest};
use sunocore::services::feed::{GetFeed, GetFeedRequest};
use sunocore::services::instruments::{GetInstruments, GetInstrumentsRequest};
use sunocore::services::key::{GetKey, GetKeyRequest};
use sunocore::services::midi::{GetMidi, GetMidiRequest};
use sunocore::services::studio::{GetStudioProject, GetStudioProjectRequest};
use sunocore::services::waveform::{GetWaveformAggregates, GetWaveformAggregatesRequest};
use sunocore::validation::env::{suno_api_key_unwrap, suno_base_url_unwrap, workspace_id_unwrap};
use tower::{Service, ServiceExt};
use uuid::Uuid;

// Small Tower service adapter around reqwest::Client so we can plug it into
// GetStudioProject without bringing in hyper.
#[derive(Clone)]
struct ReqwestSvc {
    client: reqwest::Client,
}

impl Service<Request<Empty<Bytes>>> for ReqwestSvc {
    type Response = Response<Full<Bytes>>;
    type Error = reqwest::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request<Empty<Bytes>>) -> Self::Future {
        let client = self.client.clone();
        let method = req.method().clone();
        let uri = req.uri().to_string();
        let headers = req.headers().clone();

        Box::pin(async move {
            let mut builder = client.request(method, uri);
            // Copy headers from incoming http::Request
            for (name, value) in headers.iter() {
                builder = builder.header(name, value);
            }

            let res = builder.send().await?;
            let status = res.status();
            let ct = res.headers().get(http::header::CONTENT_TYPE).cloned();
            let body = res.bytes().await?;

            let mut resp = Response::new(Full::new(Bytes::from(body)));
            *resp.status_mut() = StatusCode::from_u16(status.as_u16()).unwrap();
            if let Some(ct) = ct {
                resp.headers_mut().insert(http::header::CONTENT_TYPE, ct);
            }
            Ok(resp)
        })
    }
}

// Tower service adapter for POST requests with body
#[derive(Clone)]
struct ReqwestSvcWithBody {
    client: reqwest::Client,
}

impl Service<Request<Full<Bytes>>> for ReqwestSvcWithBody {
    type Response = Response<Full<Bytes>>;
    type Error = reqwest::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
        let client = self.client.clone();
        let method = req.method().clone();
        let uri = req.uri().to_string();
        let headers = req.headers().clone();
        let body = req.into_body();

        Box::pin(async move {
            use http_body_util::BodyExt;

            let body_bytes = body.collect().await.unwrap().to_bytes();

            let mut builder = client.request(method, uri);
            // Copy headers from incoming http::Request
            for (name, value) in headers.iter() {
                builder = builder.header(name, value);
            }
            builder = builder.body(body_bytes.to_vec());

            let res = builder.send().await?;
            let status = res.status();
            let ct = res.headers().get(http::header::CONTENT_TYPE).cloned();
            let body = res.bytes().await?;

            let mut resp = Response::new(Full::new(Bytes::from(body)));
            *resp.status_mut() = StatusCode::from_u16(status.as_u16()).unwrap();
            if let Some(ct) = ct {
                resp.headers_mut().insert(http::header::CONTENT_TYPE, ct);
            }
            Ok(resp)
        })
    }
}

// Integration test: call the real endpoint via the Tower service (using reqwest under the hood)
// and ensure it deserializes into StudioProject.
//
// Run with: cargo test --test integration_tower_service -- --ignored
#[tokio::test]
async fn fetch_and_deserialize_studio_project_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let project_id = "a6d2bb30-0004-4860-b6bb-1639ca065957".to_string();
    let expected_uuid = Uuid::parse_str(&project_id).expect("valid project id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetStudioProject::new(inner);

    let project: StudioProject = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetStudioProjectRequest {
            base_url: base,
            project_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    assert_eq!(project.id, expected_uuid, "mismatched project id");
    assert!(!project.title.is_empty(), "title should not be empty");
    assert!(
        project.state.tracks.len() > 0,
        "expected at least one track"
    );
}

// Integration test: call the downbeats endpoint via the Tower service
// and ensure it deserializes into Downbeats.
//
// Run with: cargo test --test integration_tower_service -- --ignored
#[tokio::test]
async fn fetch_and_deserialize_downbeats_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetDownbeats::new(inner);

    let downbeats: Downbeats = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetDownbeatsRequest {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    assert_eq!(
        downbeats.state,
        DownbeatsState::Complete,
        "downbeats should be in complete state"
    );
    assert!(
        downbeats.downbeats.is_some(),
        "expected downbeats array to be present"
    );

    eprintln!(
        "Downbeats response: state={:?}, has_downbeats={}",
        downbeats.state,
        downbeats.downbeats.is_some()
    );
    if let Some(db) = &downbeats.downbeats {
        assert!(db.len() > 0, "expected at least one downbeat");
        eprintln!("✓ Downbeats returned {} beats", db.len());
        if !db.is_empty() {
            eprintln!("  First beat: {:.3}s at beat {}", db[0][0], db[0][1]);
            eprintln!(
                "  Last beat: {:.3}s at beat {}",
                db[db.len() - 1][0],
                db[db.len() - 1][1]
            );
        }
        for beat in db {
            assert_eq!(beat.len(), 2, "each downbeat should have 2 elements");
            assert!(beat[0] >= 0.0, "time should be non-negative");
            assert!(
                beat[1] >= 1.0 && beat[1] <= 4.0,
                "beat number should be 1-4"
            );
        }
    }
}

// Integration test: call the key detection endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_key_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetKey::new(inner);

    let key: Key = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetKeyRequest {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!("Key response: state={:?}, key={:?}", key.state, key.key);
    assert_eq!(
        key.state,
        DownbeatsState::Complete,
        "key should be in complete state"
    );
    assert!(key.key.is_some(), "expected key to be present");
    eprintln!("✓ Key detection returned: {:?}", key.key.unwrap());
}

// Integration test: call the instruments detection endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_instruments_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetInstruments::new(inner);

    let instruments: Instruments = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetInstrumentsRequest {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!(
        "Instruments response: state={:?}, instruments={:?}",
        instruments.state, instruments.instruments
    );
    // May be running or complete depending on when test is run
    assert!(
        instruments.state == DownbeatsState::Complete
            || instruments.state == DownbeatsState::Running
    );
    if instruments.state == DownbeatsState::Complete {
        assert!(
            instruments.instruments.is_some(),
            "expected instruments to be present when complete"
        );
        eprintln!(
            "✓ Instruments detection returned {} instruments",
            instruments.instruments.as_ref().unwrap().len()
        );
    }
}

// Integration test: call the MIDI transcription endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_midi_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetMidi::new(inner);

    let midi: Midi = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetMidiRequest {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!(
        "MIDI response: state={:?}, has_instruments={}",
        midi.state,
        midi.instruments.is_some()
    );
    // May be running or complete depending on when test is run
    assert!(midi.state == DownbeatsState::Complete || midi.state == DownbeatsState::Running);
    if midi.state == DownbeatsState::Complete && midi.instruments.is_some() {
        let instruments = midi.instruments.as_ref().unwrap();
        eprintln!(
            "✓ MIDI transcription returned {} instruments",
            instruments.len()
        );
        if !instruments.is_empty() {
            eprintln!(
                "  First instrument: {} with {} notes",
                instruments[0].name,
                instruments[0].notes.len()
            );
        }
    }
}

// Integration test: call the aligned lyrics endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_aligned_lyrics_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetAlignedLyricsV3::new(inner);

    let lyrics: AlignedLyricsV3 = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetAlignedLyricsV3Request {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!(
        "Aligned lyrics response: state={:?}, has_alignment={}",
        lyrics.state,
        lyrics.alignment.is_some()
    );
    assert_eq!(
        lyrics.state,
        DownbeatsState::Complete,
        "lyrics should be in complete state"
    );
    if let Some(alignment) = &lyrics.alignment {
        eprintln!("✓ Aligned lyrics returned {} words", alignment.len());
        if !alignment.is_empty() {
            eprintln!(
                "  First word: '{}' at {:.2}s-{:.2}s",
                alignment[0].word, alignment[0].start_s, alignment[0].end_s
            );
        }
    }
}

// Integration test: call the waveform aggregates endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_waveform_aggregates_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let clip_id = "012ad0f4-d29d-4f5f-a141-83f11f94d114".to_string();
    let expected_uuid = Uuid::parse_str(&clip_id).expect("valid clip id");
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvc { client };
    let mut svc = GetWaveformAggregates::new(inner);

    let waveforms: WaveformAggregates = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetWaveformAggregatesRequest {
            base_url: base,
            clip_id: expected_uuid,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!(
        "Waveform aggregates response: {} levels",
        waveforms.waveform_aggregates.len()
    );
    assert!(
        waveforms.waveform_aggregates.len() > 0,
        "expected at least one waveform aggregate"
    );
    eprintln!(
        "✓ Waveform aggregates returned {} mipmap levels",
        waveforms.waveform_aggregates.len()
    );
    for agg in &waveforms.waveform_aggregates {
        eprintln!(
            "  Level {}: {} channels with {} samples each",
            agg.mip_map_level,
            agg.data.len(),
            if !agg.data.is_empty() {
                agg.data[0].len()
            } else {
                0
            }
        );
    }
}

// Integration test: call the feed endpoint via the Tower service
#[tokio::test]
async fn fetch_and_deserialize_feed_tower() {
    let _ = dotenv();

    let base = suno_base_url_unwrap();
    let workspace_uuid = workspace_id_unwrap();
    let api_key = suno_api_key_unwrap();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .expect("build reqwest client");

    let inner = ReqwestSvcWithBody { client };
    let mut svc = GetFeed::new(inner);

    let feed_request = FeedRequest {
        cursor: None,
        limit: 20,
        filters: FeedFilters {
            disliked: "False".to_string(),
            trashed: "False".to_string(),
            from_studio_project: PresenceFilter {
                presence: "False".to_string(),
            },
            stem: PresenceFilter {
                presence: "False".to_string(),
            },
            workspace: WorkspaceFilter {
                presence: "True".to_string(),
                workspace_id: workspace_uuid,
            },
        },
    };

    let feed: FeedResponse = svc
        .ready()
        .await
        .expect("service ready")
        .call(GetFeedRequest {
            base_url: base,
            feed_request,
            api_key,
        })
        .await
        .expect("http call succeeded");

    eprintln!(
        "Feed response: {} clips, has_more={:?}",
        feed.clips.len(),
        feed.has_more
    );

    if feed.clips.len() > 0 {
        let first_clip = &feed.clips[0];
        assert!(
            !first_clip.id.to_string().is_empty(),
            "clip id should not be empty"
        );
        assert!(
            !first_clip.audio_url.is_empty(),
            "audio_url should not be empty"
        );
        assert!(
            !first_clip.user_id.to_string().is_empty(),
            "user_id should not be empty"
        );
        eprintln!("✓ Feed returned {} clips", feed.clips.len());
        eprintln!("  First clip: {} ({})", first_clip.title, first_clip.id);
        eprintln!(
            "  Model: {} / {}",
            first_clip.major_model_version, first_clip.model_name
        );
        eprintln!("  Duration: {}s", first_clip.metadata.duration);
        if let Some(ref tags) = first_clip.display_tags {
            eprintln!("  Tags: {}", tags);
        }
    }
}
