//! A linear channel's operations (extensions/channel-lineup.md §9): `wai channel import`
//! (`channel.import`), `wai channel guide` (`channel.guide`) and `wai channel verify`
//! (`channel.verify`).
//!
//! They keep the agent rules of [`super::envelope`], as every operation of the catalogue does: the
//! exit codes, the JSON envelope with `--json`, `--dry-run` (every file through `out.rs`, nothing
//! written), and `--yes` before a file that exists is replaced. An import's or a verification's
//! refusal has the text's reason as its code (`import_gap`, `guide_mismatch`, ...) and exit 1.

#[cfg(feature = "channel_lineup")]
use super::envelope::EXIT_FAILED;
use super::envelope::{self as agent, EXIT_INTERNAL, EXIT_NOT_FOUND, EXIT_PERMISSION, OpError};
use crate::out;
use serde_json::Value;
#[cfg(feature = "channel_lineup")]
use serde_json::json;

type R = Result<bool, OpError>;

/// Print what an operation did: its envelope with `--json`, its text otherwise; in a dry run of an
/// operation that writes, with the plan of what it would have done.
#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn done(op: &str, writes: bool, data: Value, text: &str) -> R {
    let plan = (writes && out::dry_run()).then(out::plan);
    if agent::ctx().json {
        agent::print(&agent::envelope_with(op, true, data, plan));
    } else if plan.is_some() {
        println!("{text}");
        println!("dry run: nothing was written. Without --dry-run, wai would:");
        for l in out::plan_lines() {
            println!("  {l}");
        }
    } else {
        println!("{text}");
    }
    Ok(true)
}

#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn io_fail(path: &str, e: &std::io::Error, hint: &str) -> OpError {
    match e.kind() {
        std::io::ErrorKind::NotFound => OpError::new(EXIT_NOT_FOUND, "not-found", format!("{path}: {e}"), hint),
        std::io::ErrorKind::PermissionDenied => OpError::new(EXIT_PERMISSION, "permission-denied", format!("{path}: {e}"), "the file's owner runs this"),
        _ => OpError::new(EXIT_INTERNAL, "io-error", format!("{path}: {e}"), hint),
    }
}

#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn read(path: &str, what: &str) -> Result<Vec<u8>, OpError> {
    std::fs::read(path).map_err(|e| io_fail(path, &e, &format!("give {what}")))
}

/// Every value of a flag that may be repeated, in order.
#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn all<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> {
    args.windows(2).filter(|w| w[0] == flag).map(|w| w[1].as_str()).collect()
}

#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn number(args: &[String], flag: &str, usage: &str) -> Result<Option<u64>, OpError> {
    match super::opt(args, flag) {
        None => Ok(None),
        Some(v) => v.parse().map(Some).map_err(|_| OpError::usage(format!("{flag} {v}: milliseconds since 1970, an integer"), usage)),
    }
}

#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn unhex(s: &str) -> Option<Vec<u8>> {
    if !s.len().is_multiple_of(2) {
        return None;
    }
    (0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()
}

#[cfg_attr(not(feature = "channel_lineup"), allow(dead_code))]
fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// A refusal of the text, as the toolchain's failure (exit 1, the reason as its code).
#[cfg(feature = "channel_lineup")]
fn refused(r: wai::channel_lineup::Reason, what: &str) -> OpError {
    OpError::new(EXIT_FAILED, r.code(), format!("{what}: {r}"), "extensions/channel-lineup.md §10 names where each reason is raised")
}

#[cfg(not(feature = "channel_lineup"))]
pub fn import(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("channel.import").expect("listed")))
}

#[cfg(not(feature = "channel_lineup"))]
pub fn guide(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("channel.guide").expect("listed")))
}

#[cfg(not(feature = "channel_lineup"))]
pub fn verify(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("channel.verify").expect("listed")))
}

/// Write `files`, each `(path, bytes)`; replacing one that exists waits for `--yes`.
#[cfg(feature = "channel_lineup")]
fn write_all(files: &[(String, Vec<u8>)]) -> Result<(), OpError> {
    if !out::dry_run() {
        let replaces: Vec<&String> = files.iter().map(|(p, _)| p).filter(|p| std::path::Path::new(p).exists()).collect();
        if !replaces.is_empty() {
            let names: Vec<&str> = replaces.iter().map(|s| s.as_str()).collect();
            agent::confirm(&format!("{} exist(s), and would be replaced", names.join(", ")), replaces.iter().map(|p| json!({"write": p, "replaces": true})).collect())?;
        }
    }
    for (p, b) in files {
        out::write(p, b).map_err(|e| io_fail(p, &e, "check the directory"))?;
    }
    Ok(())
}

/// `wai channel import ...`: a channel's feeds into its receipt, lineup and cues (§4).
#[cfg(feature = "channel_lineup")]
pub fn import(args: &[String]) -> R {
    use std::collections::BTreeMap;
    use wai::channel_lineup::{import::{cues_json, import, Feeds}, receipt_text, MAX_DOCUMENT};
    let usage = super::usage("channel.import");
    let with_value = [
        "--channel", "--xmltv", "--mrss", "--mrss-owner", "--feed", "--playlist", "--key", "--signer-id", "--owner", "--id-system", "--xmltv-channel",
        "--start-ms", "--artwork", "--window-start", "--window-end", "--parent", "--out-dir",
    ];
    agent::check_args(args, &with_value, &with_value, 0, &usage)?;
    let channel = super::opt(args, "--channel").ok_or_else(|| OpError::usage("--channel: the channel id", &usage))?;
    let key_arg = super::opt(args, "--key").ok_or_else(|| OpError::usage("--key: the operator's key: its name in the keystore, or its file (32 bytes, or 64 hexadecimal digits)", &usage))?;
    let key = super::key::signing_key(key_arg, false)?;
    let mrss = all(args, "--mrss");
    let owners = all(args, "--mrss-owner");
    if !owners.is_empty() && owners.len() != mrss.len() {
        return Err(OpError::usage("--mrss-owner: none, or one for each --mrss, in their order", &usage));
    }
    let mut feeds = Feeds { channel: channel.to_string(), ..Feeds::default() };
    feeds.xmltv = super::opt(args, "--xmltv").map(|p| read(p, "the XMLTV document")).transpose()?;
    for (k, p) in mrss.iter().enumerate() {
        feeds.mrss.push((read(p, "the Media RSS document")?, owners.get(k).map(|o| o.to_string())));
    }
    feeds.json = super::opt(args, "--feed").map(|p| read(p, "the JSON form")).transpose()?;
    feeds.playlist = super::opt(args, "--playlist").map(|p| read(p, "the Media Playlist")).transpose()?;
    feeds.owner = super::opt(args, "--owner").map(str::to_string);
    feeds.id_system = super::opt(args, "--id-system").map(str::to_string);
    feeds.xmltv_channel = super::opt(args, "--xmltv-channel").map(str::to_string);
    feeds.start_ms = number(args, "--start-ms", &usage)?;
    feeds.window = match (number(args, "--window-start", &usage)?, number(args, "--window-end", &usage)?) {
        (Some(s), Some(e)) => Some((s, e)),
        (None, None) => None,
        _ => return Err(OpError::usage("--window-start and --window-end: both, or neither", &usage)),
    };
    if let Some(dir) = super::opt(args, "--artwork") {
        let mut art = BTreeMap::new();
        for e in std::fs::read_dir(dir).map_err(|e| io_fail(dir, &e, "give the directory that holds the artwork"))?.flatten() {
            if e.file_type().is_ok_and(|t| t.is_file()) {
                use std::io::Read;
                let mut b = Vec::new();
                let p = e.path();
                std::fs::File::open(&p).and_then(|f| f.take(MAX_DOCUMENT as u64 + 1).read_to_end(&mut b)).map_err(|x| io_fail(&p.display().to_string(), &x, "check the artwork"))?;
                art.insert(e.file_name().to_string_lossy().into_owned(), b);
            }
        }
        feeds.artwork = art;
    }
    let parent = match super::opt(args, "--parent") {
        None => None,
        Some(h) => Some(unhex(h).and_then(|b| <[u8; 32]>::try_from(b).ok()).ok_or_else(|| OpError::usage("--parent: a receipt hash, 64 hexadecimal digits", &usage))?),
    };
    let did = wai::signer_id::did_key(&key.verifying_key().to_bytes());
    let signer_id = super::opt(args, "--signer-id").unwrap_or(&did);
    let got = import(&feeds, &key, signer_id, parent).map_err(|r| refused(r, "the feeds are refused"))?;
    let dir = std::path::Path::new(super::opt(args, "--out-dir").unwrap_or("."));
    let path = |n: &str| dir.join(n).display().to_string();
    let cues = serde_json::to_string_pretty(&cues_json(&got.cues)).expect("JSON") + "\n";
    let files = vec![
        (path("channel.json"), receipt_text(&got.receipt).into_bytes()),
        (path("lineup.wlu"), got.lineup.to_bytes()),
        (path("cues.json"), cues.into_bytes()),
    ];
    if !dir.as_os_str().is_empty() {
        out::create_dir_all(dir).map_err(|e| io_fail(&dir.display().to_string(), &e, "check the directory"))?;
    }
    write_all(&files)?;
    let (r, l) = (&got.receipt, &got.lineup);
    let data = json!({
        "channel": channel,
        "receipt": files[0].0, "receipt_hash": hex(&r.receipt_hash()), "schedule_root": hex(&r.schedule_root),
        "entries": r.schedule.len(), "window": [r.window_start_ms, r.window_end_ms], "aired": r.aired.len(),
        "lineup": files[1].0, "lineup_digest": hex(&l.digest()), "programmes": l.programmes.len(), "assets": l.assets.len(),
        "cues": files[2].0, "breaks": got.cues.len(), "signer_id": r.signer_id,
    });
    let text = format!(
        "imported {channel}: {} entries, {} programmes of {} assets, {} breaks; schedule root {}…, lineup {}…\n  receipt {}\n  lineup  {}\n  cues    {}",
        r.schedule.len(),
        l.programmes.len(),
        l.assets.len(),
        got.cues.len(),
        &hex(&r.schedule_root)[..16],
        &hex(&l.digest())[..16],
        files[0].0,
        files[1].0,
        files[2].0
    );
    done("channel.import", true, data, &text)
}

/// The receipt and lineup a guide is made from, checked first (§8.2 steps 1 to 7).
#[cfg(feature = "channel_lineup")]
fn sources(args: &[String], usage: &str) -> Result<(Vec<u8>, Vec<u8>), OpError> {
    let r = super::opt(args, "--receipt").ok_or_else(|| OpError::usage("--receipt: a channel receipt in its JSON carriage", usage))?;
    let l = super::opt(args, "--lineup").ok_or_else(|| OpError::usage("--lineup: a lineup's file", usage))?;
    Ok((read(r, "the receipt")?, read(l, "the lineup")?))
}

/// `wai channel guide --receipt R --lineup L ...`: the guide (§7).
#[cfg(feature = "channel_lineup")]
pub fn guide(args: &[String]) -> R {
    use wai::channel_lineup::{now_next, read_receipt, verify, xmltv_guide, Lineup};
    let usage = super::usage("channel.guide");
    let with_value = ["--receipt", "--lineup", "--from", "--to", "--at", "--out"];
    let mut known = with_value.to_vec();
    known.push("--now-next");
    agent::check_args(args, &known, &with_value, 0, &usage)?;
    let (rb, lb) = sources(args, &usage)?;
    let v = verify(&rb, &lb, None);
    if let Err(r) = v.verdict {
        return Err(refused(r, "the receipt and the lineup do not verify"));
    }
    let r = read_receipt(&rb).expect("it verified");
    let l = Lineup::parse(&lb).expect("it verified");
    let now = args.iter().any(|a| a == "--now-next");
    let (body, format, data) = if now {
        let at = number(args, "--at", &usage)?.ok_or_else(|| OpError::usage("--now-next takes --at, an instant in milliseconds since 1970", &usage))?;
        let b = now_next(&r, &l, at).ok_or_else(|| OpError::usage("--at: at most 2^53 - 1", &usage))?;
        (b, "now-next", json!({"at_ms": at}))
    } else {
        let from = number(args, "--from", &usage)?.unwrap_or(l.schedule_start_ms);
        let to = number(args, "--to", &usage)?.unwrap_or(l.schedule_end_ms);
        let b = xmltv_guide(&r, &l, from, to).ok_or_else(|| OpError::usage("--from and --to: a range, from below to, to at most 2^53 - 1", &usage))?;
        (b, "xmltv", json!({"from_ms": from, "to_ms": to}))
    };
    let file = super::opt(args, "--out").unwrap_or(if now { "now-next.json" } else { "guide.xml" }).to_string();
    write_all(&[(file.clone(), body.clone().into_bytes())])?;
    let mut d = json!({"file": file, "format": format, "schedule_root": hex(&r.schedule_root), "lineup": hex(&l.digest()), "bytes": body.len()});
    for (k, x) in data.as_object().expect("an object") {
        d[k] = x.clone();
    }
    done("channel.guide", true, d, &format!("wrote the {format} guide of {} to {file}", l.channel_id))
}

/// `wai channel verify --receipt R --lineup L [--guide G]`: what §8.3 states.
#[cfg(feature = "channel_lineup")]
pub fn verify(args: &[String]) -> R {
    use wai::channel_lineup::{verification_json, verify};
    let usage = super::usage("channel.verify");
    let with_value = ["--receipt", "--lineup", "--guide"];
    agent::check_args(args, &with_value, &with_value, 0, &usage)?;
    let (rb, lb) = sources(args, &usage)?;
    let g = super::opt(args, "--guide").map(|p| read(p, "the guide")).transpose()?;
    let v = verify(&rb, &lb, g.as_deref());
    let facts = verification_json(&v);
    let mut text = facts["verdict"].as_str().unwrap_or("?").to_string();
    if let Some(p) = facts["part"].as_str() {
        text.push_str(&format!(" (the {p})"));
    }
    if let Some(h) = facts["receipt"].as_str() {
        text.push_str(&format!("\n  receipt {h}"));
    }
    if let Some(h) = facts["lineup"].as_str() {
        text.push_str(&format!("\n  lineup  {h}"));
    }
    for p in facts["guide"]["programmes"].as_array().into_iter().flatten() {
        text.push_str(&format!("\n  {} to {}  {}{}", p["start_ms"], p["stop_ms"], p["asset_id"].as_str().unwrap_or(""), if p["aired"] == true { "  (aired in the window)" } else { "" }));
    }
    match v.verdict {
        Ok(()) => done("channel.verify", false, facts, &text),
        Err(r) => {
            if agent::ctx().json {
                agent::print(&agent::error_envelope_with(Some("channel.verify"), &refused(r, "the channel does not verify"), Some(facts)));
                Err(OpError::reported(EXIT_FAILED))
            } else {
                println!("{text}");
                Ok(false)
            }
        }
    }
}
