tail-fin-cli 0.5.1

Multi-site browser automation CLI — attaches to Chrome or auto-launches a stealth browser to drive 15+ sites
use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{browser_session, print_json, print_list, Ctx};

#[derive(Subcommand)]
pub enum PccAction {
    /// Show API info (latest date, total records)
    Info,
    /// Search tenders by title
    Search {
        query: String,
        #[arg(long, default_value_t = 1)]
        page: u32,
    },
    /// Search tenders by company name
    SearchCompany {
        query: String,
        #[arg(long, default_value_t = 1)]
        page: u32,
    },
    /// Search tenders by company ID (unified business number)
    SearchId {
        query: String,
        #[arg(long, default_value_t = 1)]
        page: u32,
    },
    /// Search tenders by special budget
    SearchBudget {
        query: String,
        #[arg(long, default_value_t = 1)]
        page: u32,
    },
    /// List all special budgets
    Budgets,
    /// List tenders by date (YYYYMMDD)
    Date { date: String },
    /// List all government units
    Units,
    /// List tenders by government unit
    Unit {
        unit_id: String,
        #[arg(long, default_value_t = 1)]
        page: u32,
    },
    /// Get tender details
    Tender { unit_id: String, job_number: String },
}

async fn pcc_session(ctx: &Ctx) -> Result<night_fury_core::BrowserSession, TailFinError> {
    if let Some(ref host) = ctx.connect {
        browser_session(host, ctx.headed).await
    } else {
        eprintln!("Launching stealth browser for PCC (with Cloudflare bypass)...");
        // PCC has Cloudflare JS challenge — use cloudflare_timeout to wait for it
        let session = night_fury_core::BrowserSession::builder()
            .headed(ctx.headed)
            .cloudflare_timeout(std::time::Duration::from_secs(15))
            .launch_stealth("https://pcc.g0v.ronny.tw")
            .await?;
        Ok(session)
    }
}

pub async fn run(action: PccAction, ctx: &Ctx) -> Result<(), TailFinError> {
    let session = pcc_session(ctx).await?;
    let client = tail_fin_pcc::PccClient::new(session);

    match action {
        PccAction::Info => {
            let info = client.info().await?;
            print_json(&info)?;
        }
        PccAction::Search { query, page } => {
            let result = client.search(&query, page).await?;
            print_json(&result)?;
        }
        PccAction::SearchCompany { query, page } => {
            let result = client.search_company(&query, page).await?;
            print_json(&result)?;
        }
        PccAction::SearchId { query, page } => {
            let result = client.search_id(&query, page).await?;
            print_json(&result)?;
        }
        PccAction::SearchBudget { query, page } => {
            let result = client.search_budget(&query, page).await?;
            print_json(&result)?;
        }
        PccAction::Budgets => {
            let budgets = client.budgets().await?;
            print_list("budgets", &budgets, budgets.len())?;
        }
        PccAction::Date { date } => {
            let result = client.date(&date).await?;
            print_json(&result)?;
        }
        PccAction::Units => {
            let units = client.units().await?;
            print_list("units", &units, units.len())?;
        }
        PccAction::Unit { unit_id, page } => {
            let result = client.unit(&unit_id, page).await?;
            print_json(&result)?;
        }
        PccAction::Tender {
            unit_id,
            job_number,
        } => {
            let detail = client.tender(&unit_id, &job_number).await?;
            print_json(&detail)?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "pcc"
    }

    fn about(&self) -> &'static str {
        "Taiwan government procurement (PCC)"
    }

    fn command(&self) -> clap::Command {
        <PccAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("pcc").about("Taiwan government procurement (PCC)"),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <PccAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}