use std::sync::Arc;

use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::Json;
use axum::response::{IntoResponse, Response};
use rsa::{RsaPrivateKey, Pkcs1v15Sign};
use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey};
use sha2::{Sha256, Digest};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64;
use serde::{Serialize, Deserialize};

use crate::db;
use crate::routes::AppError;
use crate::session::Session;
use crate::state::AppState;

#[derive(Serialize, Deserialize, Clone)]
pub struct SolveReceipt {
    pub receipt_id: String,
    pub puzzle_hash: String,
    pub solve_time_secs: i64,
    pub hints_used: i64,
    pub solve_date: String,
    pub instance_url: String,
    pub player_name: String,
    pub room_code: Option<String>,
    pub mode: Option<String>,
    pub players: Vec<String>,
    pub created_at: String,
    pub expires_at: String,
}

#[derive(Serialize, Deserialize)]
pub struct SignedReceipt {
    pub receipt: SolveReceipt,
    pub signature: String,
    pub key_id: String,
}

#[derive(Deserialize)]
pub struct GenerateReceiptRequest {
    pub puzzle_id: String,
    pub solve_time: i64,
    pub hints_used: i64,
    pub player_name: String,
    pub room_code: Option<String>,
    pub mode: Option<String>,
    pub players: Option<Vec<String>>,
}

fn get_instance_url() -> String {
    if let Ok(url) = std::env::var("INSTANCE_URL") {
        return url;
    }
    let port = std::env::var("PORT").ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8080);
    format!("http://localhost:{}", port)
}

fn get_private_key(db: &rusqlite::Connection) -> Option<RsaPrivateKey> {
    let priv_pem: String = db.query_row(
        "SELECT private_pem FROM ap_keys WHERE id = 1",
        [],
        |row| row.get(0),
    ).ok()?;
    RsaPrivateKey::from_pkcs1_pem(&priv_pem).ok()
}

pub fn sign_receipt_data(private_key: &RsaPrivateKey, receipt_json: &[u8]) -> String {
    let hash = Sha256::digest(receipt_json);
    // DER-encoded DigestInfo for SHA-256
    let mut digest_info = vec![
        0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20,
    ];
    digest_info.extend_from_slice(&hash);
    let padding = Pkcs1v15Sign::new_unprefixed();
    let sig = private_key.sign(padding, &digest_info).expect("receipt signing failed");
    B64.encode(&sig)
}

pub fn verify_receipt_signature(public_key_pem: &str, receipt_json: &[u8], signature_b64: &str) -> Result<(), String> {
    use rsa::RsaPublicKey;
    let public_key = RsaPublicKey::from_pkcs1_pem(public_key_pem)
        .map_err(|e| format!("bad public key: {}", e))?;
    let sig_bytes = B64.decode(signature_b64)
        .map_err(|_| "bad signature base64".to_string())?;
    let hash = Sha256::digest(receipt_json);
    let mut digest_info = vec![
        0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20,
    ];
    digest_info.extend_from_slice(&hash);
    let padding = Pkcs1v15Sign::new_unprefixed();
    public_key.verify(padding, &digest_info, &sig_bytes)
        .map_err(|e| format!("signature verification failed: {}", e))
}

pub fn generate_receipt(
    db: &rusqlite::Connection,
    puzzle_hash: &str,
    solve_time: i64,
    hints: i64,
    player_name: &str,
    room_code: Option<&str>,
    mode: Option<&str>,
    other_players: Vec<String>,
) -> Option<SignedReceipt> {
    let private_key = get_private_key(db)?;
    let instance_url = get_instance_url();
    let now = chrono::Utc::now();
    let expires = now + chrono::Duration::days(30);

    let receipt = SolveReceipt {
        receipt_id: uuid::Uuid::new_v4().to_string(),
        puzzle_hash: puzzle_hash.to_string(),
        solve_time_secs: solve_time,
        hints_used: hints,
        solve_date: now.format("%Y-%m-%d").to_string(),
        instance_url: instance_url.clone(),
        player_name: player_name.to_string(),
        room_code: room_code.map(|s| s.to_string()),
        mode: mode.map(|s| s.to_string()),
        players: other_players,
        created_at: now.to_rfc3339(),
        expires_at: expires.to_rfc3339(),
    };

    let receipt_json = serde_json::to_string(&receipt).ok()?;
    let signature = sign_receipt_data(&private_key, receipt_json.as_bytes());
    let key_id = format!("{}/ap/actor#main-key", instance_url);

    let players_json = serde_json::to_string(&receipt.players).unwrap_or_else(|_| "[]".into());

    crate::db::store_receipt(
        db, &receipt.receipt_id, &signature, &receipt.expires_at,
        puzzle_hash, solve_time, hints, &receipt.solve_date,
        &instance_url, player_name, room_code, mode,
        Some(&players_json),
    );

    Some(SignedReceipt { receipt, signature, key_id })
}

// -- route handlers --

pub async fn generate_receipt_handler(
    State(state): State<Arc<AppState>>,
    Json(req): Json<GenerateReceiptRequest>,
) -> Result<Response, AppError> {
    let dbi = state.db.lock().await;

    // get puzzle JSON to compute hash
    let (puzzle_json, _) = db::get_puzzle_json(&dbi.db, &req.puzzle_id)
        .ok_or_else(|| AppError::with_status(StatusCode::NOT_FOUND, "puzzle not found"))?;
    let puzzle_hash = hex::encode(Sha256::digest(puzzle_json.as_bytes()));

    let players = req.players.unwrap_or_default();
    let signed = generate_receipt(
        &dbi.db,
        &puzzle_hash,
        req.solve_time,
        req.hints_used,
        &req.player_name,
        req.room_code.as_deref(),
        req.mode.as_deref(),
        players,
    ).ok_or_else(|| AppError::with_status(
        StatusCode::INTERNAL_SERVER_ERROR,
        "could not generate receipt - signing keys not available",
    ))?;

    let instance_url = get_instance_url();
    let receipt_url = format!("{}/api/receipts/{}", instance_url, signed.receipt.receipt_id);

    Ok(Json(serde_json::json!({
        "receipt": signed.receipt,
        "signature": signed.signature,
        "key_id": signed.key_id,
        "receipt_url": receipt_url,
        "receipt_id": signed.receipt.receipt_id,
    })).into_response())
}

pub async fn get_receipt_handler(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
) -> Result<Response, AppError> {
    let dbi = state.db.lock().await;
    let json = db::get_receipt(&dbi.db, &id)
        .ok_or_else(|| AppError::with_status(StatusCode::NOT_FOUND, "receipt not found or expired"))?;

    // add the key_id to the response
    let instance_url = get_instance_url();
    let key_id = format!("{}/ap/actor#main-key", instance_url);
    let mut val: serde_json::Value = serde_json::from_str(&json)
        .map_err(|_| AppError::with_status(StatusCode::INTERNAL_SERVER_ERROR, "corrupt receipt data"))?;
    val["key_id"] = serde_json::json!(key_id);

    Ok(Json(val).into_response())
}

#[derive(Deserialize)]
pub struct ClaimRequest {
    pub receipt: SolveReceipt,
    pub signature: String,
    pub key_id: String,
}

pub async fn claim_receipt_handler(
    State(state): State<Arc<AppState>>,
    session: Session,
    Json(req): Json<ClaimRequest>,
) -> Result<Response, AppError> {
    // check not already claimed
    {
        let dbi = state.db.lock().await;
        if db::is_receipt_claimed(&dbi.db, &req.receipt.receipt_id) {
            return Err(AppError::with_status(StatusCode::CONFLICT, "receipt already claimed"));
        }
    }

    // validate receipt data bounds
    if req.receipt.solve_time_secs < 0 || req.receipt.solve_time_secs > 172_800 {
        return Err(AppError::with_status(StatusCode::BAD_REQUEST, "invalid solve time"));
    }
    if req.receipt.hints_used < 0 || req.receipt.hints_used > 1000 {
        return Err(AppError::with_status(StatusCode::BAD_REQUEST, "invalid hints count"));
    }

    // check receipt hasn't expired
    if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&req.receipt.expires_at) {
        if chrono::Utc::now() > expires {
            return Err(AppError::with_status(StatusCode::GONE, "receipt expired"));
        }
    }

    // fetch remote public key - unless it is our own instance, in which case
    // verify against the locally stored key (receipts issued here point at
    // the configured INSTANCE_URL, which can be localhost or an internal name)
    let actor_url = req.key_id.split('#').next()
        .ok_or_else(|| AppError::with_status(StatusCode::BAD_REQUEST, "bad key_id format"))?
        .to_string();

    let actor_host = actor_url
        .split("//")
        .nth(1)
        .and_then(|s| s.split('/').next())
        .unwrap_or("");
    let instance_host = get_instance_url();
    let instance_host = instance_host
        .split("//")
        .nth(1)
        .and_then(|s| s.split('/').next())
        .unwrap_or("");
    let is_self = std::env::var("AP_DOMAIN")
        .ok()
        .filter(|d| d == actor_host)
        .is_some()
        || actor_host == instance_host;

    let public_key_pem;
    if is_self {
        let dbi = state.db.lock().await;
        match dbi.db.query_row(
            "SELECT public_pem FROM ap_keys WHERE id = 1",
            [],
            |r| r.get::<_, String>(0),
        ) {
            Ok(pem) => public_key_pem = pem,
            Err(_) => {
                return Err(AppError::with_status(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "local signing key not available",
                ))
            }
        }
    } else {
        if !actor_url.starts_with("https://") {
            return Err(AppError::with_status(StatusCode::BAD_REQUEST, "key_id must use HTTPS"));
        }
        if crate::security::is_internal_url(&actor_url) {
            return Err(AppError::with_status(StatusCode::FORBIDDEN, "internal URL not allowed"));
        }

        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .redirect(reqwest::redirect::Policy::none())
            .build()
            .map_err(|_| AppError::with_status(StatusCode::INTERNAL_SERVER_ERROR, "http client error"))?;

        let actor_json: serde_json::Value = client.get(&actor_url)
            .header("Accept", "application/activity+json, application/json")
            .send()
            .await
            .map_err(|e| AppError::with_status(StatusCode::BAD_GATEWAY, &format!("failed to fetch actor: {}", e)))?
            .json()
            .await
            .map_err(|_| AppError::with_status(StatusCode::BAD_GATEWAY, "bad actor response"))?;

        public_key_pem = actor_json
            .get("publicKey")
            .and_then(|pk| pk.get("publicKeyPem"))
            .and_then(|v| v.as_str())
            .ok_or_else(|| AppError::with_status(StatusCode::BAD_GATEWAY, "no publicKey in actor document"))?
            .to_string();
    }

    // verify signature
    let receipt_json = serde_json::to_string(&req.receipt)
        .map_err(|_| AppError::with_status(StatusCode::BAD_REQUEST, "bad receipt data"))?;

    verify_receipt_signature(&public_key_pem, receipt_json.as_bytes(), &req.signature)
        .map_err(|e| AppError::with_status(StatusCode::FORBIDDEN, &e))?;

    // claim it and record the solve
    let dbi = state.db.lock().await;
    db::claim_receipt(&dbi.db, &req.receipt.receipt_id, &session.token, &req.receipt.instance_url);
    db::record_solve(
        &dbi.db,
        &session.token,
        &format!("receipt:{}", req.receipt.receipt_id),
        req.receipt.solve_time_secs,
        req.receipt.hints_used,
        false,
        None,
    );

    Ok(Json(serde_json::json!({
        "claimed": true,
        "solve_time": req.receipt.solve_time_secs,
        "hints": req.receipt.hints_used,
    })).into_response())
}
