#![allow(dead_code)]

use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::{
    Arc, Mutex,
    atomic::{AtomicBool, Ordering},
};
use std::thread::{self, JoinHandle};
use std::time::Duration;

use serde_json::{Value, json};

pub const PROJECT_ID: &str = "412";
pub const MERGE_REQUEST_IID: &str = "3995";
pub const GITHUB_PROJECT_KEY: &str = "ArthurianX/mr-milchick";
pub const PULL_REQUEST_NUMBER: &str = "3995";

#[derive(Debug, Clone)]
struct MockGithubFile {
    path: String,
    previous_path: Option<String>,
    status: String,
    additions: Option<u32>,
    deletions: Option<u32>,
    patch: Option<String>,
}

#[derive(Debug, Clone)]
struct RequestRecord {
    method: String,
    path: String,
    body: String,
}

#[derive(Debug, Clone)]
struct MockNote {
    id: u64,
    body: String,
}

#[derive(Debug, Clone)]
struct MockSlackMessage {
    channel: String,
    text: String,
    ts: String,
    thread_ts: Option<String>,
}

#[derive(Debug)]
struct ServerState {
    reviewers: Vec<String>,
    notes: Vec<MockNote>,
    slack_messages: Vec<MockSlackMessage>,
    github_files: Vec<MockGithubFile>,
    request_log: Vec<RequestRecord>,
    next_note_id: u64,
    next_slack_ts: u64,
}

pub struct MockGitLabServer {
    address: SocketAddr,
    running: Arc<AtomicBool>,
    state: Arc<Mutex<ServerState>>,
    handle: Option<JoinHandle<()>>,
}

impl MockGitLabServer {
    pub fn start() -> Self {
        Self::start_with_reviewers_and_github_files(Vec::new(), 1)
    }

    pub fn start_with_reviewers(reviewers: Vec<&str>) -> Self {
        Self::start_with_reviewers_and_github_files(reviewers, 1)
    }

    pub fn start_with_github_file_count(file_count: usize) -> Self {
        Self::start_with_reviewers_and_github_files(Vec::new(), file_count)
    }

    pub fn start_with_reviewers_and_github_files(reviewers: Vec<&str>, file_count: usize) -> Self {
        let listener = TcpListener::bind("127.0.0.1:0").expect("mock server should bind");
        listener
            .set_nonblocking(true)
            .expect("mock server should be nonblocking");
        let address = listener
            .local_addr()
            .expect("mock server should have address");
        let running = Arc::new(AtomicBool::new(true));
        let state = Arc::new(Mutex::new(ServerState {
            reviewers: reviewers
                .into_iter()
                .map(|value| value.to_string())
                .collect(),
            notes: Vec::new(),
            slack_messages: Vec::new(),
            github_files: github_files(file_count),
            request_log: Vec::new(),
            next_note_id: 1,
            next_slack_ts: 1,
        }));

        let handle = {
            let running = Arc::clone(&running);
            let state = Arc::clone(&state);

            thread::spawn(move || {
                while running.load(Ordering::SeqCst) {
                    match listener.accept() {
                        Ok((mut stream, _)) => handle_connection(&mut stream, &state),
                        Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
                            thread::sleep(Duration::from_millis(10));
                        }
                        Err(err) => panic!("mock server accept failed: {err}"),
                    }
                }
            })
        };

        Self {
            address,
            running,
            state,
            handle: Some(handle),
        }
    }

    pub fn api_base_url(&self) -> String {
        format!("http://{}/api/v4", self.address)
    }

    pub fn slack_api_base_url(&self) -> String {
        format!("http://{}/slack/api", self.address)
    }

    pub fn github_api_base_url(&self) -> String {
        format!("http://{}/api/github", self.address)
    }

    pub fn request_count(&self, method: &str, path: &str) -> usize {
        self.state
            .lock()
            .expect("state lock should succeed")
            .request_log
            .iter()
            .filter(|record| record.method == method && record.path == path)
            .count()
    }

    pub fn request_count_prefix(&self, method: &str, path_prefix: &str) -> usize {
        self.state
            .lock()
            .expect("state lock should succeed")
            .request_log
            .iter()
            .filter(|record| record.method == method && record.path.starts_with(path_prefix))
            .count()
    }

    pub fn note_bodies(&self) -> Vec<String> {
        self.state
            .lock()
            .expect("state lock should succeed")
            .notes
            .iter()
            .map(|note| note.body.clone())
            .collect()
    }

    pub fn assigned_reviewers(&self) -> Vec<String> {
        self.state
            .lock()
            .expect("state lock should succeed")
            .reviewers
            .clone()
    }

    pub fn request_bodies(&self, method: &str, path: &str) -> Vec<String> {
        self.state
            .lock()
            .expect("state lock should succeed")
            .request_log
            .iter()
            .filter(|record| record.method == method && record.path == path)
            .map(|record| record.body.clone())
            .collect()
    }
}

impl Drop for MockGitLabServer {
    fn drop(&mut self) {
        self.running.store(false, Ordering::SeqCst);
        let _ = TcpStream::connect(self.address);

        if let Some(handle) = self.handle.take() {
            handle.join().expect("mock server should join cleanly");
        }
    }
}

fn handle_connection(stream: &mut TcpStream, state: &Arc<Mutex<ServerState>>) {
    stream
        .set_nonblocking(false)
        .expect("accepted stream should be switched to blocking mode");

    let request = read_http_request(stream);
    let mut guard = state.lock().expect("state lock should succeed");
    guard.request_log.push(RequestRecord {
        method: request.method.clone(),
        path: request.path.clone(),
        body: request.body.clone(),
    });

    let response = route_request(&request, &mut guard);
    write_http_response(stream, response.status_code, &response.body);
}

#[derive(Debug)]
struct HttpRequest {
    method: String,
    path: String,
    body: String,
}

#[derive(Debug)]
struct HttpResponse {
    status_code: u16,
    body: String,
}

fn read_http_request(stream: &mut TcpStream) -> HttpRequest {
    stream
        .set_read_timeout(Some(Duration::from_secs(2)))
        .expect("stream timeout should be set");

    let mut buffer = Vec::new();
    let mut temp = [0_u8; 1024];

    loop {
        let read = read_from_stream(stream, &mut temp, "request read should succeed");
        if read == 0 {
            break;
        }

        buffer.extend_from_slice(&temp[..read]);

        if let Some(header_end) = find_header_end(&buffer) {
            let headers = String::from_utf8_lossy(&buffer[..header_end]).to_string();
            let content_length = headers
                .lines()
                .find_map(|line| {
                    let (name, value) = line.split_once(':')?;
                    if name.eq_ignore_ascii_case("content-length") {
                        value.trim().parse::<usize>().ok()
                    } else {
                        None
                    }
                })
                .unwrap_or(0);

            let total_length = header_end + 4 + content_length;
            while buffer.len() < total_length {
                let read = read_from_stream(stream, &mut temp, "request body read should succeed");
                if read == 0 {
                    break;
                }
                buffer.extend_from_slice(&temp[..read]);
            }

            let request_line = headers
                .lines()
                .next()
                .expect("request should contain a request line");
            let mut parts = request_line.split_whitespace();
            let method = parts
                .next()
                .expect("request should have method")
                .to_string();
            let path = parts.next().expect("request should have path").to_string();
            let body =
                String::from_utf8_lossy(&buffer[header_end + 4..header_end + 4 + content_length])
                    .to_string();

            return HttpRequest { method, path, body };
        }
    }

    panic!("incomplete HTTP request received by mock server");
}

fn read_from_stream(stream: &mut TcpStream, buffer: &mut [u8], error_message: &str) -> usize {
    loop {
        match stream.read(buffer) {
            Ok(read) => return read,
            Err(err)
                if matches!(
                    err.kind(),
                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
                ) =>
            {
                thread::sleep(Duration::from_millis(10));
            }
            Err(err) => panic!("{error_message}: {err}"),
        }
    }
}

fn find_header_end(buffer: &[u8]) -> Option<usize> {
    buffer.windows(4).position(|window| window == b"\r\n\r\n")
}

fn write_http_response(stream: &mut TcpStream, status_code: u16, body: &str) {
    let status_text = match status_code {
        200 => "OK",
        201 => "Created",
        404 => "Not Found",
        500 => "Internal Server Error",
        _ => "OK",
    };

    let response = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        status_code,
        status_text,
        body.len(),
        body
    );

    stream
        .write_all(response.as_bytes())
        .expect("response write should succeed");
}

fn route_request(request: &HttpRequest, state: &mut ServerState) -> HttpResponse {
    let mr_path = format!("/api/v4/projects/{PROJECT_ID}/merge_requests/{MERGE_REQUEST_IID}");
    let notes_path = format!("{mr_path}/notes");
    let changes_path = format!("{mr_path}/changes");
    let pr_path = format!("/api/github/repos/{GITHUB_PROJECT_KEY}/pulls/{PULL_REQUEST_NUMBER}");
    let requested_reviewers_path = format!("{pr_path}/requested_reviewers");
    let files_path = format!("{pr_path}/files");
    let issue_comments_path =
        format!("/api/github/repos/{GITHUB_PROJECT_KEY}/issues/{PULL_REQUEST_NUMBER}/comments");
    let slack_post_message_path = "/slack/api/chat.postMessage";
    let slack_history_path = "/slack/api/conversations.history";

    match (request.method.as_str(), request.path.as_str()) {
        ("GET", path) if path == mr_path => HttpResponse {
            status_code: 200,
            body: json!({
                "iid": 3995,
                "title": "Frontend adjustments",
                "description": "Refresh the UI flow",
                "state": "opened",
                "draft": false,
                "web_url": "https://gitlab.example.com/group/project/-/merge_requests/3995",
                "author": {"username": "arthur"},
                "reviewers": state.reviewers.iter().map(|username| json!({"username": username})).collect::<Vec<_>>(),
            })
            .to_string(),
        },
        ("GET", path) if path == changes_path => HttpResponse {
            status_code: 200,
            body: json!({
                "changes": [
                    {
                        "old_path": "apps/frontend/button_old.tsx",
                        "new_path": "apps/frontend/button.tsx",
                        "new_file": false,
                        "renamed_file": false,
                        "deleted_file": false,
                        "diff": "@@ -1,2 +1,2 @@"
                    }
                ]
            })
            .to_string(),
        },
        ("GET", path) if path == pr_path => HttpResponse {
            status_code: 200,
            body: json!({
                "number": 3995,
                "title": "Frontend adjustments",
                "body": "Refresh the UI flow",
                "state": "open",
                "draft": false,
                "html_url": "https://github.com/ArthurianX/mr-milchick/pull/3995",
                "user": {"login": "arthur"},
                "requested_reviewers": state.reviewers.iter().map(|username| json!({"login": username})).collect::<Vec<_>>(),
                "labels": [{"name": "frontend"}],
            })
            .to_string(),
        },
        ("GET", path) if path.starts_with(&files_path) => {
            let page = query_param(path, "page")
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(1);
            let per_page = query_param(path, "per_page")
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(100);
            let start = per_page.saturating_mul(page.saturating_sub(1));
            let files = state
                .github_files
                .iter()
                .skip(start)
                .take(per_page)
                .map(|file| {
                    json!({
                        "filename": file.path,
                        "previous_filename": file.previous_path,
                        "status": file.status,
                        "additions": file.additions,
                        "deletions": file.deletions,
                        "patch": file.patch,
                    })
                })
                .collect::<Vec<_>>();

            HttpResponse {
                status_code: 200,
                body: json!(files).to_string(),
            }
        }
        ("GET", path) if path == notes_path => HttpResponse {
            status_code: 200,
            body: json!(
                state
                    .notes
                    .iter()
                    .map(|note| json!({"id": note.id, "body": note.body}))
                    .collect::<Vec<_>>()
            )
            .to_string(),
        },
        ("GET", path) if path.starts_with(&issue_comments_path) => {
            let page = query_param(path, "page")
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(1);
            let per_page = query_param(path, "per_page")
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(100);
            let start = per_page.saturating_mul(page.saturating_sub(1));
            let comments = state
                .notes
                .iter()
                .skip(start)
                .take(per_page)
                .map(|note| json!({"id": note.id, "body": note.body}))
                .collect::<Vec<_>>();

            HttpResponse {
                status_code: 200,
                body: json!(comments).to_string(),
            }
        }
        ("GET", path) if path.starts_with("/api/v4/users?username=") => {
            let username = path
                .split_once("username=")
                .map(|(_, value)| value)
                .unwrap_or_default();
            let user = user_lookup(username);
            HttpResponse {
                status_code: 200,
                body: json!([user]).to_string(),
            }
        }
        ("PUT", path) if path == mr_path => {
            let body: Value =
                serde_json::from_str(&request.body).expect("reviewer assignment should be JSON");
            let reviewer_ids = body["reviewer_ids"]
                .as_array()
                .expect("reviewer_ids should be an array");

            state.reviewers = reviewer_ids
                .iter()
                .map(|value| {
                    let id = value.as_u64().expect("reviewer id should be a number");
                    username_for_id(id).to_string()
                })
                .collect();

            HttpResponse {
                status_code: 200,
                body: "{}".to_string(),
            }
        }
        ("POST", path) if path == requested_reviewers_path => {
            let body: Value =
                serde_json::from_str(&request.body).expect("reviewer request should be JSON");
            let reviewers = body["reviewers"]
                .as_array()
                .expect("reviewers should be an array");

            state.reviewers = reviewers
                .iter()
                .map(|value| {
                    value
                        .as_str()
                        .expect("reviewer username should be a string")
                        .to_string()
                })
                .collect();

            HttpResponse {
                status_code: 200,
                body: "{}".to_string(),
            }
        }
        ("POST", path) if path == notes_path => {
            let body: Value =
                serde_json::from_str(&request.body).expect("note creation should be JSON");
            let note_body = body["body"]
                .as_str()
                .expect("note body should be a string")
                .to_string();

            let note_id = state.next_note_id;
            state.next_note_id += 1;
            state.notes.push(MockNote {
                id: note_id,
                body: note_body,
            });

            HttpResponse {
                status_code: 201,
                body: json!({"id": note_id}).to_string(),
            }
        }
        ("POST", path) if path == issue_comments_path => {
            let body: Value =
                serde_json::from_str(&request.body).expect("comment creation should be JSON");
            let note_body = body["body"]
                .as_str()
                .expect("comment body should be a string")
                .to_string();

            let note_id = state.next_note_id;
            state.next_note_id += 1;
            state.notes.push(MockNote {
                id: note_id,
                body: note_body,
            });

            HttpResponse {
                status_code: 201,
                body: json!({"id": note_id}).to_string(),
            }
        }
        ("PUT", path) if path.starts_with(&format!("{notes_path}/")) => {
            let note_id = path
                .rsplit('/')
                .next()
                .expect("note path should include id")
                .parse::<u64>()
                .expect("note id should be numeric");
            let body: Value =
                serde_json::from_str(&request.body).expect("note update should be JSON");
            let note_body = body["body"]
                .as_str()
                .expect("note body should be a string")
                .to_string();

            let note = state
                .notes
                .iter_mut()
                .find(|note| note.id == note_id)
                .expect("existing note should be present for update");
            note.body = note_body;

            HttpResponse {
                status_code: 200,
                body: json!({"id": note_id}).to_string(),
            }
        }
        ("PATCH", path)
            if path.starts_with(&format!(
                "/api/github/repos/{GITHUB_PROJECT_KEY}/issues/comments/"
            )) =>
        {
            let note_id = path
                .rsplit('/')
                .next()
                .expect("comment path should include id")
                .parse::<u64>()
                .expect("comment id should be numeric");
            let body: Value =
                serde_json::from_str(&request.body).expect("comment update should be JSON");
            let note_body = body["body"]
                .as_str()
                .expect("comment body should be a string")
                .to_string();

            let note = state
                .notes
                .iter_mut()
                .find(|note| note.id == note_id)
                .expect("existing comment should be present for update");
            note.body = note_body;

            HttpResponse {
                status_code: 200,
                body: json!({"id": note_id}).to_string(),
            }
        }
        ("POST", path) if path == slack_post_message_path => {
            let body: Value =
                serde_json::from_str(&request.body).expect("Slack post should be JSON");
            let ts = format!("1700000000.{:06}", state.next_slack_ts);
            state.next_slack_ts += 1;
            state.slack_messages.push(MockSlackMessage {
                channel: body["channel"]
                    .as_str()
                    .expect("Slack channel should be a string")
                    .to_string(),
                text: body["text"]
                    .as_str()
                    .expect("Slack text should be a string")
                    .to_string(),
                ts: ts.clone(),
                thread_ts: body["thread_ts"].as_str().map(ToString::to_string),
            });

            HttpResponse {
                status_code: 200,
                body: json!({
                    "ok": true,
                    "channel": "C0ALY38CW3X",
                    "ts": ts,
                    "message": {
                        "text": "posted"
                    }
                })
                .to_string(),
            }
        }
        ("GET", path) if path.starts_with(slack_history_path) => {
            let channel = query_param(path, "channel").unwrap_or_default();
            let messages = state
                .slack_messages
                .iter()
                .rev()
                .filter(|message| message.channel == channel)
                .map(|message| {
                    let mut payload = json!({
                        "type": "message",
                        "text": message.text,
                        "ts": message.ts,
                    });

                    if let Some(thread_ts) = &message.thread_ts {
                        payload["thread_ts"] = json!(thread_ts);
                    }

                    payload
                })
                .collect::<Vec<_>>();

            HttpResponse {
                status_code: 200,
                body: json!({
                    "ok": true,
                    "messages": messages,
                    "response_metadata": {
                        "next_cursor": ""
                    }
                })
                .to_string(),
            }
        }
        ("POST", path) if path.starts_with("/slack/api/webhook/") => {
            let ts = format!("1700000000.{:06}", state.next_slack_ts);
            state.next_slack_ts += 1;

            HttpResponse {
                status_code: 200,
                body: json!({
                    "ok": true,
                    "ts": ts
                })
                .to_string(),
            }
        }
        _ => HttpResponse {
            status_code: 404,
            body: json!({"error": "unmatched route", "path": request.path}).to_string(),
        },
    }
}

fn github_files(count: usize) -> Vec<MockGithubFile> {
    (0..count)
        .map(|index| MockGithubFile {
            path: if index == 0 {
                "apps/frontend/button.tsx".to_string()
            } else {
                format!("packages/pkg-{index}/src/lib.rs")
            },
            previous_path: None,
            status: "modified".to_string(),
            additions: Some(10),
            deletions: Some(2),
            patch: Some("@@ -1,2 +1,2 @@".to_string()),
        })
        .collect()
}

fn query_param<'a>(path: &'a str, key: &str) -> Option<&'a str> {
    let query = path.split_once('?')?.1;
    query.split('&').find_map(|pair| {
        let (name, value) = pair.split_once('=')?;
        (name == key).then_some(value)
    })
}

fn user_lookup(username: &str) -> Value {
    json!({
        "id": user_id_for_username(username),
        "username": username
    })
}

fn user_id_for_username(username: &str) -> u64 {
    match username {
        "principal-reviewer" => 1001,
        "bob" => 1002,
        "milchick-duty" => 1003,
        "alice" => 1004,
        other => panic!("unexpected user lookup for '{other}'"),
    }
}

fn username_for_id(id: u64) -> &'static str {
    match id {
        1001 => "principal-reviewer",
        1002 => "bob",
        1003 => "milchick-duty",
        1004 => "alice",
        other => panic!("unexpected reviewer id '{other}'"),
    }
}

pub fn base_env(server: &MockGitLabServer) -> Vec<(&'static str, String)> {
    vec![
        ("CI_PROJECT_ID", PROJECT_ID.to_string()),
        ("CI_MERGE_REQUEST_IID", MERGE_REQUEST_IID.to_string()),
        ("CI_PIPELINE_SOURCE", "merge_request_event".to_string()),
        (
            "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME",
            "feat/intentional-cleanup".to_string(),
        ),
        (
            "CI_MERGE_REQUEST_TARGET_BRANCH_NAME",
            "develop".to_string(),
        ),
        ("CI_MERGE_REQUEST_LABELS", "".to_string()),
        ("GITLAB_TOKEN", "test-token".to_string()),
        ("GITLAB_BASE_URL", server.api_base_url()),
        (
            "MR_MILCHICK_REVIEWERS",
            r#"[{"username":"milchick-duty","fallback":true},{"username":"principal-reviewer","mandatory":true},{"username":"bob","areas":["frontend"]}]"#.to_string(),
        ),
        ("MR_MILCHICK_MAX_REVIEWERS", "2".to_string()),
        ("MR_MILCHICK_CODEOWNERS_ENABLED", "false".to_string()),
        ("MR_MILCHICK_SLACK_ENABLED", "false".to_string()),
        ("RUST_LOG", "off".to_string()),
    ]
}

pub fn borrow_env<'a>(envs: &'a [(&'static str, String)]) -> Vec<(&'a str, &'a str)> {
    envs.iter()
        .map(|(key, value)| (*key, value.as_str()))
        .collect()
}

pub fn github_base_env(server: &MockGitLabServer) -> Vec<(&'static str, String)> {
    vec![
        ("GITHUB_ACTIONS", "true".to_string()),
        ("GITHUB_EVENT_NAME", "pull_request".to_string()),
        ("GITHUB_REPOSITORY", GITHUB_PROJECT_KEY.to_string()),
        (
            "GITHUB_EVENT_PATH",
            write_github_event_payload().display().to_string(),
        ),
        ("GITHUB_TOKEN", "test-token".to_string()),
        ("GITHUB_API_BASE_URL", server.github_api_base_url()),
        (
            "MR_MILCHICK_REVIEWERS",
            r#"[{"username":"milchick-duty","fallback":true},{"username":"principal-reviewer","mandatory":true},{"username":"bob","areas":["frontend"]}]"#.to_string(),
        ),
        ("MR_MILCHICK_MAX_REVIEWERS", "2".to_string()),
        ("MR_MILCHICK_CODEOWNERS_ENABLED", "false".to_string()),
        ("MR_MILCHICK_SLACK_ENABLED", "false".to_string()),
        ("RUST_LOG", "off".to_string()),
    ]
}

fn write_github_event_payload() -> std::path::PathBuf {
    let unique = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock should be after epoch")
        .as_nanos();
    let path = std::env::temp_dir().join(format!("mr-milchick-github-event-{unique}.json"));
    std::fs::write(
        &path,
        json!({
            "number": 3995,
            "pull_request": {
                "number": 3995,
                "head": { "ref": "feat/intentional-cleanup" },
                "base": { "ref": "develop" },
                "labels": [{"name": "frontend"}]
            }
        })
        .to_string(),
    )
    .expect("github event payload should be written");
    path
}
