use actix_web::{Error, HttpResponse, web};
use async_stream::stream;
use futures_util::Stream;
use futures_util::StreamExt;
use models::repos::{repo, repo_branch_protect};
use sea_orm::*;
use std::path::PathBuf;
use std::pin::Pin;
use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;

use db::database::AppDatabase;

type ByteStream = Pin<Box<dyn Stream<Item = Result<Vec<u8>, std::io::Error>>>>;

const PRE_PACK_LIMIT: usize = 1_048_576;
const GIT_OPERATION_TIMEOUT: Duration = Duration::from_secs(30);

pub fn is_valid_oid(oid: &str) -> bool {
    oid.len() == 40 && oid.chars().all(|c| c.is_ascii_hexdigit())
}

/// Validate a Git LFS OID (base64-encoded SHA-256 hash, ~44 chars).
pub fn is_valid_lfs_oid(oid: &str) -> bool {
    // base64 alphabet: A-Z, a-z, 0-9, +, /, = (padding)
    (43..=44).contains(&oid.len())
        && oid.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
}

pub struct GitHttpHandler {
    storage_path: PathBuf,
    repo: repo::Model,
    db: AppDatabase,
}

impl GitHttpHandler {
    pub fn new(storage_path: PathBuf, repo: repo::Model, db: AppDatabase) -> Self {
        Self {
            storage_path,
            repo,
            db,
        }
    }

    pub async fn upload_pack(&self, payload: web::Payload) -> Result<HttpResponse, Error> {
        self.handle_git_rpc("upload-pack", payload).await
    }

    pub async fn receive_pack(&self, payload: web::Payload) -> Result<HttpResponse, Error> {
        self.handle_git_rpc("receive-pack", payload).await
    }

    pub async fn info_refs(&self, service: &str) -> Result<HttpResponse, Error> {
        let git_cmd = match service {
            "git-upload-pack" => "upload-pack",
            "git-receive-pack" => "receive-pack",
            _ => {
                return Err(actix_web::error::ErrorBadRequest("Invalid service"));
            }
        };

        let output = tokio::time::timeout(GIT_OPERATION_TIMEOUT, async {
            tokio::process::Command::new("git")
                .arg(git_cmd)
                .arg("--stateless-rpc")
                .arg("--advertise-refs")
                .arg(&self.storage_path)
                .output()
                .await
        })
        .await
        .map_err(|_| actix_web::error::ErrorInternalServerError("Git info-refs timeout"))?
        .map_err(|e| {
            actix_web::error::ErrorInternalServerError(format!("Failed to execute git: {}", e))
        })?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(actix_web::error::ErrorInternalServerError(format!(
                "Git command failed: {}",
                stderr
            )));
        }

        let mut response_body = Vec::new();
        let header = format!("# service={}\n", service);
        write_pkt_line(&mut response_body, header.as_bytes());
        write_flush_pkt(&mut response_body);
        response_body.extend_from_slice(&output.stdout);

        Ok(HttpResponse::Ok()
            .content_type(format!("application/x-{}-advertisement", service))
            .insert_header(("Cache-Control", "no-cache"))
            .body(response_body))
    }

    async fn handle_git_rpc(
        &self,
        service: &str,
        mut payload: web::Payload,
    ) -> Result<HttpResponse, Error> {
        let started = Instant::now();
        tracing::info!("git_rpc_started service={} repo={} repo_id={}", service, self.repo.repo_name, self.repo.id.to_string());
        let mut child = tokio::process::Command::new("git")
            .arg(service)
            .arg("--stateless-rpc")
            .arg(&self.storage_path)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .kill_on_drop(true)
            .spawn()
            .map_err(|e| {
                actix_web::error::ErrorInternalServerError(format!("Failed to spawn git: {}", e))
            })?;

        let stream = stream! {
            while let Some(chunk) = payload.next().await {
                match chunk {
                    Ok(bytes) => { yield Ok(bytes.to_vec()); }
                    Err(e) => { yield Err(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())); }
                }
            }
        };
        let mut stream: ByteStream = Box::pin(stream);

        if service == "receive-pack" {
            let branch_protects = repo_branch_protect::Entity::find()
                .filter(repo_branch_protect::Column::Repo.eq(self.repo.id))
                .all(self.db.reader())
                .await
                .map_err(|e| actix_web::error::ErrorInternalServerError(e.to_string()))?;

            const PACK_SIG: &[u8] = b"PACK";
            let mut pre_pack: Vec<u8> = Vec::with_capacity(65536);

            while let Some(chunk) = stream.next().await {
                let bytes = match chunk {
                    Ok(b) => b,
                    Err(e) => return Err(Error::from(e)),
                };

                // Reject oversized pre-PACK data to prevent memory exhaustion
                if pre_pack.len() + bytes.len() > PRE_PACK_LIMIT {
                    tracing::warn!("git_rpc_payload_too_large service={} repo={} repo_id={}", service, self.repo.repo_name, self.repo.id.to_string());
                    return Err(actix_web::error::ErrorPayloadTooLarge(format!(
                        "Ref negotiation exceeds {} byte limit",
                        PRE_PACK_LIMIT
                    )));
                }

                if let Some(pos) = bytes.windows(4).position(|w| w == PACK_SIG) {
                    pre_pack.extend_from_slice(&bytes[..pos]);

                    if let Err(msg) = check_branch_protection(&branch_protects, &pre_pack) {
                        tracing::warn!("branch_protection_violation repo={} repo_id={} message={}", self.repo.repo_name, self.repo.id.to_string(), msg);
                        return Err(actix_web::error::ErrorForbidden(msg));
                    }

                    let remaining: ByteStream = Box::pin(stream! {
                        yield Ok(bytes[pos..].to_vec());
                        while let Some(chunk) = stream.next().await {
                            yield chunk;
                        }
                    });
                    stream = remaining;
                    break;
                } else {
                    pre_pack.extend_from_slice(&bytes);
                }
            }
        }

        if let Some(mut stdin) = child.stdin.take() {
            let write_task = actix_web::rt::spawn(async move {
                while let Some(chunk) = stream.next().await {
                    match chunk {
                        Ok(bytes) => {
                            if let Err(e) = stdin.write_all(&bytes).await {
                                return Err(e);
                            }
                        }
                        Err(e) => {
                            return Err(std::io::Error::new(std::io::ErrorKind::Other, e));
                        }
                    }
                }
                drop(stdin);
                Ok::<_, std::io::Error>(())
            });

            let write_result = tokio::time::timeout(GIT_OPERATION_TIMEOUT, write_task)
                .await
                .map_err(|_| actix_web::error::ErrorInternalServerError("Git stdin write timeout"))?
                .map_err(|e| {
                    actix_web::error::ErrorInternalServerError(format!("Write error: {}", e))
                })?;

            if let Err(e) = write_result {
                return Err(actix_web::error::ErrorInternalServerError(format!(
                    "Failed to write to git: {}",
                    e
                )));
            }
        }

        let output = tokio::time::timeout(GIT_OPERATION_TIMEOUT, child.wait_with_output())
            .await
            .map_err(|_| actix_web::error::ErrorInternalServerError("Git operation timeout"))?
            .map_err(|e| {
                actix_web::error::ErrorInternalServerError(format!("Git wait failed: {}", e))
            })?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let ms = started.elapsed().as_millis() as u64;
            tracing::error!("git_rpc_failed service={} repo={} repo_id={} duration_ms={} stderr={}", service, self.repo.repo_name, self.repo.id.to_string(), ms, stderr.to_string());
            return Err(actix_web::error::ErrorInternalServerError(format!(
                "Git command failed: {}",
                stderr
            )));
        }

        let ms = started.elapsed().as_millis() as u64;
        tracing::info!("git_rpc_completed service={} repo={} repo_id={} duration_ms={} bytes_out={}", service, self.repo.repo_name, self.repo.id.to_string(), ms, output.stdout.len());

        Ok(HttpResponse::Ok()
            .content_type(format!("application/x-git-{}-result", service))
            .insert_header(("Cache-Control", "no-cache"))
            .body(output.stdout))
    }
}

fn write_pkt_line(buf: &mut Vec<u8>, data: &[u8]) {
    let len = data.len() + 4;
    buf.extend_from_slice(format!("{:04x}", len).as_bytes());
    buf.extend_from_slice(data);
}

fn write_flush_pkt(buf: &mut Vec<u8>) {
    buf.extend_from_slice(b"0000");
}

#[derive(Debug)]
struct RefUpdate {
    old_oid: Option<String>,
    new_oid: Option<String>,
    name: String,
}

fn check_branch_protection(
    branch_protects: &[repo_branch_protect::Model],
    pre_pack: &[u8],
) -> Result<(), String> {
    let refs = parse_ref_updates(pre_pack)?;
    for r#ref in &refs {
        for protection in branch_protects {
            // Match exactly or as directory prefix (e.g. "refs/heads/main"
            // matches "refs/heads/main" and "refs/heads/main/*" but NOT
            // "refs/heads/main-v2").
            let matches = r#ref.name == protection.branch
                || r#ref.name.starts_with(&format!("{}/", protection.branch));
            if !matches {
                continue;
            }
            // Check deletion (new_oid is all zeros / 40 zeros)
            if r#ref.new_oid.as_deref() == Some("0000000000000000000000000000000000000000") {
                if protection.forbid_deletion {
                    return Err(format!(
                        "Deletion of protected branch '{}' is forbidden",
                        r#ref.name
                    ));
                }
                continue;
            }

            // Check tag push
            if r#ref.name.starts_with("refs/tags/") {
                if protection.forbid_tag_push {
                    return Err(format!(
                        "Tag push to protected branch '{}' is forbidden",
                        r#ref.name
                    ));
                }
                continue;
            }

            // Check force push: old != new AND old is non-zero (non-fast-forward update)
            if let (Some(old_oid), Some(new_oid)) =
                (r#ref.old_oid.as_deref(), r#ref.new_oid.as_deref())
            {
                let is_new_branch = old_oid == "0000000000000000000000000000000000000000";
                if !is_new_branch
                    && old_oid != new_oid
                    && r#ref.name.starts_with("refs/heads/")
                    && protection.forbid_force_push
                {
                    return Err(format!(
                        "Force push to protected branch '{}' is forbidden",
                        r#ref.name
                    ));
                }
            }

            // Check push
            if protection.forbid_push {
                return Err(format!(
                    "Push to protected branch '{}' is forbidden",
                    r#ref.name
                ));
            }
        }
    }
    Ok(())
}

fn parse_ref_updates(buffer: &[u8]) -> Result<Vec<RefUpdate>, String> {
    let text = String::from_utf8_lossy(buffer);
    let mut refs = Vec::new();

    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') || line.starts_with("PACK") {
            continue;
        }
        // Format: "<old-oid> <new-oid> <ref-name>\n"
        let mut parts = line.split_whitespace();
        let old_oid = parts.next().map(|s| s.to_string());
        let new_oid = parts.next().map(|s| s.to_string());
        let name = parts
            .next()
            .unwrap_or("")
            .trim_start_matches('\0')
            .to_string();
        if !name.is_empty() {
            refs.push(RefUpdate {
                old_oid,
                new_oid,
                name,
            });
        }
    }

    Ok(refs)
}
