// ssl-client.go — slogical SSL REST API サンプルクライアント
//
// 使い方:
//
//	go run ssl-client.go order [フラグ]   # 新規注文（後払い）
//	go run ssl-client.go reissue   [フラグ]   # 再発行
//	go run ssl-client.go status    [フラグ]   # 注文ステータス確認
//	go run ssl-client.go download  [フラグ]   # 証明書ダウンロード
//
// 動作要件:
//   - Go 1.20 以上（外部ライブラリ不要）
//   - DNS 認証で Route53 を自動設定する場合は AWS CLI が必要
//     （aws configure で認証情報を設定済みであること）
//
// API ドキュメント: https://www.slogical.co.jp/ssl/api/docs/
// サンプル解説:     https://www.slogical.co.jp/ssl/api/docs/sample/
package main

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

const apiBase = "https://www.slogical.co.jp/ssl/api/v1"

// ─────────────────────────────────────────────
// 型定義
// ─────────────────────────────────────────────

// State はステートファイル（デフォルト: ssl-state.json）に保存する実行状態。
// 中断しても次回実行時にここから続きを再開できる。
type State struct {
	Action  string `json:"action"` // "order" | "reissue"
	Step    string `json:"step"`   // "pending-dcv" | "completed"
	OrderID int    `json:"order_id"`
	Dcv     string `json:"dcv"` // "email" | "file" | "dns"
	Token   string `json:"token,omitempty"`
	Domain  string `json:"domain"`

	// ファイル認証
	FileAuthPath string `json:"file_auth_path,omitempty"`

	// DNS 認証
	DnsRecordName   string `json:"dns_record_name,omitempty"`
	DnsRecordValue  string `json:"dns_record_value,omitempty"`
	Route53ChangeID string `json:"route53_change_id,omitempty"`
	DnsZoneID       string `json:"dns_zone_id,omitempty"`
}

// apiResp は全エンドポイント共通のレスポンス構造体
type apiResp struct {
	Status                  string   `json:"status"`
	IsSandbox               bool     `json:"is_sandbox"`
	OrderID                 int      `json:"order_id"`
	DigicertOrderID         int      `json:"digicert_order_id"`
	OrderStatus             string   `json:"order_status"`
	Token                   string   `json:"token"`
	Certificate             string   `json:"certificate"`
	IntermediateCertificate string   `json:"intermediate_certificate"`
	Message                 []string `json:"message"`
}

// POST /orders リクエスト構造体
type orderReq struct {
	Certificate orderCertReq `json:"certificate"`
	Org         orderOrgReq  `json:"organization"`
	Contact     orderCtcReq  `json:"contact"`
}

type orderCertReq struct {
	Product       string `json:"product"`
	Years         int    `json:"years"`
	Csr           string `json:"csr"`
	HashAlgorithm string `json:"hash_algorithm"`
	Sans          string `json:"sans,omitempty"`
	WwwOption     string `json:"www_option"`
	Dcv           string `json:"dcv"`
	DcvDetail     string `json:"dcv_detail,omitempty"`
	IsUpdate      string `json:"is_update"`
	ServerType    string `json:"server_type"`
	Servers       int    `json:"servers"`
}

type orderOrgReq struct {
	CorpName     string `json:"corp_name"`
	CorpNameAlph string `json:"corp_name_alph,omitempty"`
	Name1        string `json:"name1"`
	Name2        string `json:"name2"`
	NameAlph1    string `json:"name_alph1,omitempty"`
	NameAlph2    string `json:"name_alph2,omitempty"`
	Tel          string `json:"tel"`
	Email        string `json:"email"`
	Post         string `json:"post,omitempty"`
	Addr1        string `json:"addr1,omitempty"`
	Addr2        string `json:"addr2,omitempty"`
	Addr3        string `json:"addr3,omitempty"`
}

type orderCtcReq struct {
	EmailInvoice string `json:"email_invoice,omitempty"`
	IsPostpay    string `json:"is_postpay"`
}

// POST /orders/{id}/reissue リクエスト構造体
type reissueReq struct {
	Certificate reissueCertReq `json:"certificate"`
}

type reissueCertReq struct {
	Csr           string `json:"csr"`
	HashAlgorithm string `json:"hash_algorithm"`
	Dcv           string `json:"dcv"`
	DcvDetail     string `json:"dcv_detail,omitempty"`
}

// ─────────────────────────────────────────────
// ユーティリティ
// ─────────────────────────────────────────────

// logf は時刻付きでメッセージを標準出力に出力する
func logf(format string, args ...any) {
	ts := time.Now().Format("15:04:05")
	allArgs := make([]any, 0, 1+len(args))
	allArgs = append(allArgs, ts)
	allArgs = append(allArgs, args...)
	fmt.Printf("[%s] "+format+"\n", allArgs...)
}

// generateCSR は RSA 2048bit の秘密鍵と CSR を生成し、ファイルに保存する。
// 秘密鍵は 0600 権限で保存。
func generateCSR(domain, keyFile, csrFile string) (csrPEM string, err error) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return "", fmt.Errorf("秘密鍵の生成に失敗しました: %w", err)
	}

	keyPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(key),
	})
	if err = os.WriteFile(keyFile, keyPEM, 0600); err != nil {
		return "", fmt.Errorf("秘密鍵の保存に失敗しました (%s): %w", keyFile, err)
	}

	tmpl := &x509.CertificateRequest{Subject: pkix.Name{CommonName: domain}}
	csrDER, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
	if err != nil {
		return "", fmt.Errorf("CSR の生成に失敗しました: %w", err)
	}
	csrPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}))
	if err = os.WriteFile(csrFile, []byte(csrPEM), 0644); err != nil {
		return "", fmt.Errorf("CSR の保存に失敗しました (%s): %w", csrFile, err)
	}
	return csrPEM, nil
}

// apiClient は 30 秒タイムアウト付きの HTTP クライアント。
// DefaultClient をそのまま使うと API 接続が固まったとき無期限に停止するため専用クライアントを使う。
var apiClient = &http.Client{Timeout: 30 * time.Second}

// callAPI は指定したメソッド・URL で API を呼び出す。
// body が nil の場合はリクエストボディなし。
func callAPI(method, url, apiKey string, body any) (*apiResp, error) {
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("リクエスト JSON 変換失敗: %w", err)
		}
		r = bytes.NewReader(b)
	}

	req, err := http.NewRequest(method, url, r)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "ApiKey "+apiKey)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := apiClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("HTTP リクエスト失敗: %w", err)
	}
	defer resp.Body.Close()

	raw, _ := io.ReadAll(resp.Body)
	var ar apiResp
	if err = json.Unmarshal(raw, &ar); err != nil {
		return nil, fmt.Errorf("API レスポンス解析失敗 (HTTP %d): %s", resp.StatusCode, string(raw))
	}
	return &ar, nil
}

// saveState はステートをファイルに保存する。失敗した場合はエラーを返す。
func saveState(filename string, s State) error {
	b, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return fmt.Errorf("ステート JSON 変換失敗: %w", err)
	}
	if err = os.WriteFile(filename, b, 0644); err != nil {
		return fmt.Errorf("ステートファイルの書き込み失敗 (%s): %w", filename, err)
	}
	return nil
}

// mustSaveState はステートを保存し、失敗したら終了する。
// 注文受付直後・DCV 設定直後など「ここで失敗すると再開できなくなる」箇所で使用。
func mustSaveState(filename string, s State) {
	if err := saveState(filename, s); err != nil {
		fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
		fmt.Fprintf(os.Stderr, "注文 ID %d は作成済みです。手動で注文 ID を控えてください。\n", s.OrderID)
		os.Exit(1)
	}
}

// loadState はステートファイルを読み込む
func loadState(filename string) (State, error) {
	var s State
	b, err := os.ReadFile(filename)
	if err != nil {
		return s, err
	}
	return s, json.Unmarshal(b, &s)
}

// waitEnter は Enter キーが押されるまで待機する
func waitEnter(msg string) {
	fmt.Printf("\n  %s ... ", msg)
	bufio.NewReader(os.Stdin).ReadString('\n') //nolint
}

// ─────────────────────────────────────────────
// DCV 関連
// ─────────────────────────────────────────────

// setupDCV は認証方法に応じてファイル作成・DNS 設定・メール案内を行う。
// state の DnsZoneID, FileAuthPath, DnsRecordName, DnsRecordValue, Route53ChangeID を更新する。
func setupDCV(state *State, dcvEmail, zoneID string) error {
	switch state.Dcv {

	case "file":
		path := state.FileAuthPath
		if path == "" {
			path = ".well-known/pki-validation/fileauth.txt"
			state.FileAuthPath = path
		}
		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
			return fmt.Errorf("ディレクトリ作成失敗: %w", err)
		}
		if err := os.WriteFile(path, []byte(state.Token), 0644); err != nil {
			return fmt.Errorf("認証ファイルの書き込み失敗: %w", err)
		}
		fmt.Println()
		fmt.Println("━━ ファイル認証 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
		fmt.Printf("  作成ファイル: %s\n", path)
		fmt.Printf("  ファイル内容: %s\n", state.Token)
		fmt.Println()
		fmt.Println("  このファイルを Web サーバーのドキュメントルートに配置し、")
		fmt.Printf("  以下の URL でアクセスできるようにしてください:\n")
		fmt.Printf("    http://%s/.well-known/pki-validation/fileauth.txt\n", state.Domain)
		fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")

	case "dns":
		recordName := "_dnsauth." + state.Domain
		state.DnsRecordName = recordName
		state.DnsRecordValue = state.Token

		if zoneID != "" {
			state.DnsZoneID = zoneID
			logf("Route53 に TXT レコードを作成しています: %s", recordName)
			changeID, err := r53UpsertTXT(zoneID, recordName, state.Token)
			if err != nil {
				return err
			}
			state.Route53ChangeID = changeID
			logf("Route53 TXT レコード作成完了 (ChangeID: %s)", changeID)
			fmt.Println("  DNS の伝播を待っています（通常 1〜5 分）...")
		} else {
			fmt.Println()
			fmt.Println("━━ DNS 認証（手動設定）━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
			fmt.Printf("  名前  : %s\n", recordName)
			fmt.Printf("  タイプ: TXT\n")
			fmt.Printf("  値    : %s\n", state.Token)
			fmt.Printf("  TTL   : 60\n")
			fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
			waitEnter("DNS レコードを設定したら Enter を押してください")
		}

	default: // email
		fmt.Println()
		fmt.Println("━━ メール認証 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
		if dcvEmail != "" {
			fmt.Printf("  %s 宛に認証メールが送られます。\n", dcvEmail)
		} else {
			fmt.Println("  DCV メールアドレスに認証メールが送られます。")
		}
		fmt.Println("  メール内の承認リンクをクリックしてください。")
		fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
	}
	return nil
}

// r53ListTXT は Route53 から指定名の TXT レコードの現在値一覧を取得する（ダブルクォート除去済み）。
// --start-record-name / --start-record-type TXT でピンポイントに取得するため、
// Hosted Zone 全体を走査するページング問題を回避できる。
func r53ListTXT(zoneID, name string) ([]string, error) {
	out, err := exec.Command(
		"aws", "route53", "list-resource-record-sets",
		"--hosted-zone-id", zoneID,
		"--start-record-name", name,
		"--start-record-type", "TXT",
		"--max-items", "1",
		"--output", "json",
	).Output()
	if err != nil {
		return nil, fmt.Errorf("aws route53 list-resource-record-sets 失敗: %v", err)
	}
	var result struct {
		ResourceRecordSets []struct {
			Name            string `json:"Name"`
			Type            string `json:"Type"`
			ResourceRecords []struct {
				Value string `json:"Value"`
			} `json:"ResourceRecords"`
		} `json:"ResourceRecordSets"`
	}
	if err := json.Unmarshal(out, &result); err != nil {
		return nil, fmt.Errorf("Route53 レスポンス解析失敗: %w", err)
	}
	// Route53 は名前に末尾ドットを付けて返す
	target := strings.TrimSuffix(name, ".") + "."
	var values []string
	for _, rrs := range result.ResourceRecordSets {
		if rrs.Name == target && rrs.Type == "TXT" {
			for _, rr := range rrs.ResourceRecords {
				// TXT 値は "\"actual\"" 形式なのでダブルクォートを除去
				values = append(values, strings.Trim(rr.Value, `"`))
			}
		}
	}
	return values, nil
}

// buildTXTChangeBatch は change-resource-record-sets に渡す JSON を構築する。
func buildTXTChangeBatch(action, name string, values []string) string {
	records := make([]string, len(values))
	for i, v := range values {
		// Route53 TXT 値はダブルクォートで囲む必要があるため %q でエスケープ
		records[i] = fmt.Sprintf(`{"Value":%q}`, `"`+v+`"`)
	}
	return fmt.Sprintf(
		`{"Changes":[{"Action":%q,"ResourceRecordSet":{"Name":%q,"Type":"TXT","TTL":60,"ResourceRecords":[%s]}}]}`,
		action, name, strings.Join(records, ","),
	)
}

// r53UpsertTXT は Route53 の TXT RRset に value を追加する。
// 既存値がある場合はマージして UPSERT するため、他の TXT 値を消さない。
// AWS CLI がインストールされ aws configure で設定済みであること。
func r53UpsertTXT(zoneID, name, value string) (changeID string, err error) {
	existing, err := r53ListTXT(zoneID, name)
	if err != nil {
		return "", fmt.Errorf("既存 TXT レコードの確認に失敗しました（処理を中断して既存レコードを保護します）: %w", err)
	}
	merged := existing
	found := false
	for _, v := range existing {
		if v == value {
			found = true
			break
		}
	}
	if !found {
		merged = append(merged, value)
	}

	out, err := exec.Command(
		"aws", "route53", "change-resource-record-sets",
		"--hosted-zone-id", zoneID,
		"--change-batch", buildTXTChangeBatch("UPSERT", name, merged),
	).Output()
	if err != nil {
		return "", fmt.Errorf(
			"aws route53 コマンドが失敗しました: %v\n"+
				"  AWS CLI がインストールされ、aws configure で認証情報が設定済みか確認してください", err)
	}
	var res struct {
		ChangeInfo struct {
			Id string `json:"Id"`
		} `json:"ChangeInfo"`
	}
	_ = json.Unmarshal(out, &res)
	return res.ChangeInfo.Id, nil
}

// r53DeleteTXT は Route53 の TXT RRset から value だけを除去する。
// 他の TXT 値が残る場合は UPSERT で更新し、最後の値であれば RRset を DELETE する。
func r53DeleteTXT(zoneID, name, value string) {
	existing, err := r53ListTXT(zoneID, name)
	if err != nil || len(existing) == 0 {
		fmt.Printf("  Route53 TXT レコードが見つかりません（手動で確認してください）: %s\n", name)
		return
	}

	// 追加した値のみ除去
	remaining := make([]string, 0, len(existing))
	for _, v := range existing {
		if v != value {
			remaining = append(remaining, v)
		}
	}

	var action string
	var targets []string
	if len(remaining) == 0 {
		action, targets = "DELETE", existing // RRset ごと削除
	} else {
		action, targets = "UPSERT", remaining // 残りの値で上書き
	}

	out, cmdErr := exec.Command(
		"aws", "route53", "change-resource-record-sets",
		"--hosted-zone-id", zoneID,
		"--change-batch", buildTXTChangeBatch(action, name, targets),
	).CombinedOutput()
	if cmdErr != nil {
		fmt.Printf("  TXT レコードの削除に失敗しました（手動で削除してください）: %v\n  %s\n", cmdErr, string(out))
	} else {
		logf("Route53 TXT レコードを削除しました: %s", name)
	}
}

// ─────────────────────────────────────────────
// ポーリング・証明書取得
// ─────────────────────────────────────────────

// pollUntilIssued は order_status が "issued" になるまでポーリングする。
func pollUntilIssued(apiKey string, state *State, pollSec int) error {
	url := fmt.Sprintf("%s/orders/%d", apiBase, state.OrderID)
	logf("証明書の発行を待っています（%d 秒ごとに確認）...", pollSec)

	for {
		ar, err := callAPI("GET", url, apiKey, nil)
		if err != nil {
			logf("確認エラー（リトライします）: %v", err)
		} else if ar.Status == "NG" {
			return fmt.Errorf("API エラー: %s", strings.Join(ar.Message, " / "))
		} else {
			switch ar.OrderStatus {
			case "issued":
				logf("✓ 証明書が発行されました！")
				if ar.Token != "" {
					state.Token = ar.Token
				}
				return nil
			case "rejected":
				return fmt.Errorf("注文が却下されました。弊社サポートにお問い合わせください（注文 ID: %d）", state.OrderID)
			case "":
				logf("DigiCert 処理中...")
			default:
				logf("ステータス: %s", ar.OrderStatus)
			}
		}
		time.Sleep(time.Duration(pollSec) * time.Second)
	}
}

// downloadCert は証明書を取得して cert_{id}.pem と chain_{id}.pem に保存する。
func downloadCert(apiKey string, orderID int) error {
	url := fmt.Sprintf("%s/certificates/%d", apiBase, orderID)
	ar, err := callAPI("GET", url, apiKey, nil)
	if err != nil {
		return err
	}
	if ar.Status == "NG" {
		return fmt.Errorf("証明書の取得に失敗しました: %s", strings.Join(ar.Message, " / "))
	}

	certFile := fmt.Sprintf("cert_%d.pem", orderID)
	if err = os.WriteFile(certFile, []byte(ar.Certificate), 0644); err != nil {
		return fmt.Errorf("証明書の保存に失敗しました (%s): %w", certFile, err)
	}
	logf("証明書を保存しました: %s", certFile)

	if ar.IntermediateCertificate != "" {
		chainFile := fmt.Sprintf("chain_%d.pem", orderID)
		if err = os.WriteFile(chainFile, []byte(ar.IntermediateCertificate), 0644); err != nil {
			return fmt.Errorf("中間証明書の保存に失敗しました (%s): %w", chainFile, err)
		}
		logf("中間証明書を保存しました: %s", chainFile)
	}
	return nil
}

// finalize は発行完了後の後処理（証明書ダウンロード・DNS クリーンアップ・ステート更新）を行う。
func finalize(apiKey, stateFile string, state *State) {
	if err := downloadCert(apiKey, state.OrderID); err != nil {
		fmt.Fprintf(os.Stderr, "\n証明書の保存に失敗しました: %v\n", err)
		fmt.Fprintf(os.Stderr, "以下のコマンドで再取得してください:\n")
		fmt.Fprintf(os.Stderr, "  go run ssl-client.go download -api-key <KEY> -order-id %d\n", state.OrderID)
	}

	// Route53 の TXT レコードを発行後に削除
	if state.DnsZoneID != "" && state.DnsRecordName != "" && state.DnsRecordValue != "" {
		logf("Route53 の TXT レコードを削除しています...")
		r53DeleteTXT(state.DnsZoneID, state.DnsRecordName, state.DnsRecordValue)
	}

	state.Step = "completed"
	if err := saveState(stateFile, *state); err != nil {
		logf("警告: ステートファイルの更新に失敗しました（証明書は保存済み）: %v", err)
	}
	logf("完了しました。ステートファイル: %s", stateFile)
}

// resumeFlow は既存のステートファイルから処理を再開する。
// 発行済みの場合は証明書のダウンロードのみ、未発行の場合は DCV 設定をやり直してポーリング。
func resumeFlow(state State, apiKey, stateFile, dcvEmail, zoneID string, pollSec int) {
	logf("注文 ID %d の処理を再開します（DCV: %s）", state.OrderID, state.Dcv)

	if state.Step == "completed" {
		logf("この注文はすでに完了しています（cert_%d.pem を確認してください）", state.OrderID)
		return
	}

	// 現在のステータスを確認
	url := fmt.Sprintf("%s/orders/%d", apiBase, state.OrderID)
	ar, err := callAPI("GET", url, apiKey, nil)
	if err == nil && ar.Status == "OK" && ar.OrderStatus == "issued" {
		logf("✓ すでに発行済みです")
		finalize(apiKey, stateFile, &state)
		return
	}

	// GET で token が取得できた場合はステートを更新
	if ar != nil && ar.Token != "" && state.Token == "" {
		state.Token = ar.Token
		saveState(stateFile, state)
	}

	// Route53 の zone ID は CLI 引数を優先、なければステートから取得
	effectiveZoneID := zoneID
	if effectiveZoneID == "" {
		effectiveZoneID = state.DnsZoneID
	}

	// DCV を再設定（ファイルの再作成・DNS レコード再確認・メール再案内）
	if err := setupDCV(&state, dcvEmail, effectiveZoneID); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	mustSaveState(stateFile, state)

	if err := pollUntilIssued(apiKey, &state, pollSec); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	finalize(apiKey, stateFile, &state)
}

// ─────────────────────────────────────────────
// サブコマンド: order
// ─────────────────────────────────────────────

func cmdOrder(args []string) {
	fs := flag.NewFlagSet("order", flag.ExitOnError)

	// 共通
	apiKey := fs.String("api-key", os.Getenv("SSL_API_KEY"), "API キー（環境変数 SSL_API_KEY でも指定可）")
	stateFile := fs.String("state-file", "ssl-state.json", "ステートファイルのパス（途中再開に使用）")
	pollSec := fs.Int("poll-interval", 30, "ポーリング間隔（秒）")

	// 証明書
	product := fs.String("product", "RapidSSL", "製品名: RapidSSL / RapidSSLWildcard / QuickSSLPremium / QuickSSLPremiumWildcard")
	years := fs.Int("years", 1, "有効年数（1〜3）")
	domain := fs.String("domain", "", "ドメイン名（CN）【必須】")
	sans := fs.String("sans", "", "追加ドメイン（スペース区切り）")
	dcv := fs.String("dcv", "email", "認証方法: email / file / dns")
	dcvEmail := fs.String("dcv-email", "", "メール認証アドレス（例: admin@example.com）")
	zoneID := fs.String("dns-zone-id", "", "Route53 Hosted Zone ID（dns 認証で自動設定する場合）")

	// 組織情報
	corpName := fs.String("corp-name", "", "会社名（日本語）")
	corpNameAlph := fs.String("corp-name-alph", "", "会社名（英語）")
	name1 := fs.String("name1", "", "担当者 姓")
	name2 := fs.String("name2", "", "担当者 名")
	nameAlph1 := fs.String("name-alph1", "", "担当者 姓（英語）")
	nameAlph2 := fs.String("name-alph2", "", "担当者 名（英語）")
	tel := fs.String("tel", "", "電話番号")
	email := fs.String("email", "", "担当者メールアドレス")
	post := fs.String("post", "", "郵便番号")
	addr1 := fs.String("addr1", "", "住所（都道府県）")
	addr2 := fs.String("addr2", "", "住所（市区町村）")
	addr3 := fs.String("addr3", "", "住所（番地）")
	invoiceEmail := fs.String("invoice-email", "", "請求先メールアドレス（省略時は担当者メールと同じ）")

	if err := fs.Parse(args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	if *apiKey == "" {
		fmt.Fprintln(os.Stderr, "エラー: -api-key が必要です（または環境変数 SSL_API_KEY を設定してください）")
		os.Exit(1)
	}

	// ステートファイルが存在し、未完了の order がある場合はレジューム
	if state, err := loadState(*stateFile); err == nil && state.OrderID > 0 && state.Action == "order" && state.Step != "completed" {
		fmt.Printf("ステートファイルを検出しました（注文 ID: %d）。続きから再開します。\n", state.OrderID)
		resumeFlow(state, *apiKey, *stateFile, *dcvEmail, *zoneID, *pollSec)
		return
	}

	if *domain == "" {
		fmt.Fprintln(os.Stderr, "エラー: -domain が必要です")
		os.Exit(1)
	}

	// CSR・秘密鍵の生成
	// 既存ファイルがある場合は上書きを防ぐため停止する
	for _, f := range []string{"ssl-client.key", "ssl-client.csr"} {
		if _, statErr := os.Stat(f); statErr == nil {
			fmt.Fprintf(os.Stderr, "エラー: %s がすでに存在します。\n", f)
			fmt.Fprintln(os.Stderr, "  前回発行分のファイルを上書きしないよう、別のディレクトリで実行するか、")
			fmt.Fprintln(os.Stderr, "  既存ファイルを退避してから再実行してください。")
			os.Exit(1)
		}
	}
	logf("CSR と秘密鍵を生成しています（CN: %s）...", *domain)
	csrPEM, err := generateCSR(*domain, "ssl-client.key", "ssl-client.csr")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	logf("秘密鍵: ssl-client.key  /  CSR: ssl-client.csr")

	// 請求先メールのデフォルト
	inv := *invoiceEmail
	if inv == "" {
		inv = *email
	}

	// 注文リクエスト
	body := orderReq{
		Certificate: orderCertReq{
			Product:       *product,
			Years:         *years,
			Csr:           csrPEM,
			HashAlgorithm: "sha256",
			Sans:          *sans,
			WwwOption:     "a",
			Dcv:           *dcv,
			DcvDetail:     *dcvEmail,
			IsUpdate:      "n",
			ServerType:    "その他",
			Servers:       1,
		},
		Org: orderOrgReq{
			CorpName:     *corpName,
			CorpNameAlph: *corpNameAlph,
			Name1:        *name1,
			Name2:        *name2,
			NameAlph1:    *nameAlph1,
			NameAlph2:    *nameAlph2,
			Tel:          *tel,
			Email:        *email,
			Post:         *post,
			Addr1:        *addr1,
			Addr2:        *addr2,
			Addr3:        *addr3,
		},
		Contact: orderCtcReq{
			EmailInvoice: inv,
			IsPostpay:    "y",
		},
	}

	logf("注文を送信しています...")
	ar, err := callAPI("POST", apiBase+"/orders", *apiKey, body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "API エラー: %v\n", err)
		os.Exit(1)
	}
	if ar.Status != "OK" || ar.OrderID == 0 {
		fmt.Fprintf(os.Stderr, "注文エラー:\n  %s\n", strings.Join(ar.Message, "\n  "))
		os.Exit(1)
	}
	logf("注文受付完了  注文 ID: %d  DigiCert 注文 ID: %d", ar.OrderID, ar.DigicertOrderID)

	// ステート保存（ここで失敗すると注文 ID が記録されず再開不能になるため強制終了）
	state := State{
		Action:  "order",
		Step:    "pending-dcv",
		OrderID: ar.OrderID,
		Dcv:     *dcv,
		Token:   ar.Token,
		Domain:  *domain,
	}
	mustSaveState(*stateFile, state)

	// DCV 設定
	if err := setupDCV(&state, *dcvEmail, *zoneID); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	mustSaveState(*stateFile, state)

	// 発行まで待機
	if err := pollUntilIssued(*apiKey, &state, *pollSec); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	finalize(*apiKey, *stateFile, &state)
}

// ─────────────────────────────────────────────
// サブコマンド: reissue
// ─────────────────────────────────────────────

func cmdReissue(args []string) {
	fs := flag.NewFlagSet("reissue", flag.ExitOnError)

	apiKey := fs.String("api-key", os.Getenv("SSL_API_KEY"), "API キー（環境変数 SSL_API_KEY でも指定可）")
	orderID := fs.Int("order-id", 0, "再発行する注文 ID（省略時はステートファイルから読み取り）")
	domain := fs.String("domain", "", "ドメイン名（CN）（省略時はステートファイルから読み取り）")
	dcv := fs.String("dcv", "email", "認証方法: email / file / dns")
	dcvEmail := fs.String("dcv-email", "", "メール認証アドレス")
	zoneID := fs.String("dns-zone-id", "", "Route53 Hosted Zone ID")
	stateFile := fs.String("state-file", "ssl-state.json", "ステートファイルのパス")
	pollSec := fs.Int("poll-interval", 30, "ポーリング間隔（秒）")

	if err := fs.Parse(args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	if *apiKey == "" {
		fmt.Fprintln(os.Stderr, "エラー: -api-key が必要です")
		os.Exit(1)
	}

	// 既存ステートが再発行状態ならレジューム
	if state, err := loadState(*stateFile); err == nil && state.OrderID > 0 && state.Action == "reissue" && state.Step != "completed" {
		fmt.Printf("ステートファイルを検出しました（注文 ID: %d、再発行）。続きから再開します。\n", state.OrderID)
		resumeFlow(state, *apiKey, *stateFile, *dcvEmail, *zoneID, *pollSec)
		return
	}

	// 注文 ID の決定（CLI 引数 > ステートファイル）
	oid := *orderID
	dom := *domain
	if oid == 0 {
		if state, err := loadState(*stateFile); err == nil && state.OrderID > 0 {
			oid = state.OrderID
			if dom == "" {
				dom = state.Domain
			}
		}
	}
	if oid == 0 {
		fmt.Fprintln(os.Stderr, "エラー: -order-id を指定するか、ステートファイル（-state-file）に注文 ID が必要です")
		os.Exit(1)
	}
	if dom == "" {
		fmt.Fprintln(os.Stderr, "エラー: -domain が必要です")
		os.Exit(1)
	}

	// 新しい CSR・秘密鍵を生成
	// 既存ファイルがある場合は上書きを防ぐため停止する
	for _, f := range []string{"ssl-client-reissue.key", "ssl-client-reissue.csr"} {
		if _, statErr := os.Stat(f); statErr == nil {
			fmt.Fprintf(os.Stderr, "エラー: %s がすでに存在します。\n", f)
			fmt.Fprintln(os.Stderr, "  前回再発行分のファイルを上書きしないよう、別のディレクトリで実行するか、")
			fmt.Fprintln(os.Stderr, "  既存ファイルを退避してから再実行してください。")
			os.Exit(1)
		}
	}
	logf("新しい CSR と秘密鍵を生成しています（CN: %s）...", dom)
	csrPEM, err := generateCSR(dom, "ssl-client-reissue.key", "ssl-client-reissue.csr")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	logf("秘密鍵: ssl-client-reissue.key  /  CSR: ssl-client-reissue.csr")

	body := reissueReq{
		Certificate: reissueCertReq{
			Csr:           csrPEM,
			HashAlgorithm: "sha256",
			Dcv:           *dcv,
			DcvDetail:     *dcvEmail,
		},
	}

	logf("再発行リクエストを送信しています（注文 ID: %d）...", oid)
	url := fmt.Sprintf("%s/orders/%d/reissue", apiBase, oid)
	ar, err := callAPI("POST", url, *apiKey, body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "API エラー: %v\n", err)
		os.Exit(1)
	}
	if ar.Status != "OK" {
		fmt.Fprintf(os.Stderr, "再発行エラー:\n  %s\n", strings.Join(ar.Message, "\n  "))
		os.Exit(1)
	}
	logf("再発行リクエストを受け付けました")

	// ステート保存（ここで失敗すると再開不能になるため強制終了）
	state := State{
		Action:  "reissue",
		Step:    "pending-dcv",
		OrderID: oid,
		Dcv:     *dcv,
		Token:   ar.Token,
		Domain:  dom,
	}
	mustSaveState(*stateFile, state)

	// DCV 設定
	if err := setupDCV(&state, *dcvEmail, *zoneID); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	mustSaveState(*stateFile, state)

	// 発行まで待機
	if err := pollUntilIssued(*apiKey, &state, *pollSec); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	finalize(*apiKey, *stateFile, &state)
}

// ─────────────────────────────────────────────
// サブコマンド: status
// ─────────────────────────────────────────────

func cmdStatus(args []string) {
	fs := flag.NewFlagSet("status", flag.ExitOnError)
	apiKey := fs.String("api-key", os.Getenv("SSL_API_KEY"), "API キー")
	orderID := fs.Int("order-id", 0, "注文 ID（省略時はステートファイルから読み取り）")
	stateFile := fs.String("state-file", "ssl-state.json", "ステートファイルのパス")
	if err := fs.Parse(args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	if *apiKey == "" {
		fmt.Fprintln(os.Stderr, "エラー: -api-key が必要です")
		os.Exit(1)
	}

	oid := *orderID
	if oid == 0 {
		state, err := loadState(*stateFile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "ステートファイルが見つかりません（%s）。-order-id を指定してください\n", *stateFile)
			os.Exit(1)
		}
		oid = state.OrderID
	}

	url := fmt.Sprintf("%s/orders/%d", apiBase, oid)
	ar, err := callAPI("GET", url, *apiKey, nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "API エラー: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("注文 ID     : %d\n", oid)
	if ar.DigicertOrderID > 0 {
		fmt.Printf("DigiCert ID : %d\n", ar.DigicertOrderID)
	}
	status := ar.OrderStatus
	if status == "" {
		status = "処理中（DigiCert 注文処理待ち）"
	}
	fmt.Printf("ステータス  : %s\n", status)
	if len(ar.Message) > 0 {
		fmt.Printf("メッセージ  : %s\n", strings.Join(ar.Message, " / "))
	}
}

// ─────────────────────────────────────────────
// サブコマンド: download
// ─────────────────────────────────────────────

func cmdDownload(args []string) {
	fs := flag.NewFlagSet("download", flag.ExitOnError)
	apiKey := fs.String("api-key", os.Getenv("SSL_API_KEY"), "API キー")
	orderID := fs.Int("order-id", 0, "注文 ID（省略時はステートファイルから読み取り）")
	stateFile := fs.String("state-file", "ssl-state.json", "ステートファイルのパス")
	if err := fs.Parse(args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	if *apiKey == "" {
		fmt.Fprintln(os.Stderr, "エラー: -api-key が必要です")
		os.Exit(1)
	}

	oid := *orderID
	if oid == 0 {
		state, err := loadState(*stateFile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "ステートファイルが見つかりません（%s）。-order-id を指定してください\n", *stateFile)
			os.Exit(1)
		}
		oid = state.OrderID
	}

	if err := downloadCert(*apiKey, oid); err != nil {
		fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
		os.Exit(1)
	}
}

// ─────────────────────────────────────────────
// エントリポイント
// ─────────────────────────────────────────────

func usage() {
	fmt.Print(`slogical SSL API サンプルクライアント

使い方:
  go run ssl-client.go <コマンド> [フラグ]

コマンド:
  order    新規注文（後払い）
  reissue      再発行
  status       注文ステータスの確認
  download     発行済み証明書のダウンロード

共通フラグ:
  -api-key string        API キー（環境変数 SSL_API_KEY でも指定可）
  -state-file string     ステートファイルのパス（デフォルト: ssl-state.json）
  -poll-interval int     ポーリング間隔・秒（デフォルト: 30）

order の主要フラグ:
  -product string        製品名（デフォルト: RapidSSL）
  -years int             有効年数 1〜3（デフォルト: 1）
  -domain string         ドメイン名（必須）
  -dcv string            認証方法: email / file / dns（デフォルト: email）
  -dcv-email string      メール認証アドレス（例: admin@example.com）
  -invoice-email string  請求先メール
  -corp-name string      会社名（日本語）
  -corp-name-alph string 会社名（英語）
  -name1 string          担当者 姓
  -name2 string          担当者 名
  -name-alph1 string     担当者 姓（英語）
  -name-alph2 string     担当者 名（英語）
  -tel string            電話番号
  -email string          担当者メールアドレス
  -post string           郵便番号
  -addr1 string          住所（都道府県）
  -addr2 string          住所（市区町村）
  -addr3 string          住所（番地）
  -dns-zone-id string    Route53 Hosted Zone ID（dns 認証で自動設定）

reissue の主要フラグ:
  -order-id int          再発行する注文 ID
  -domain string         ドメイン名
  -dcv string            認証方法
  -dcv-email string      メール認証アドレス
  -dns-zone-id string    Route53 Hosted Zone ID

詳しい使い方: https://www.slogical.co.jp/ssl/api/docs/sample/
`)
}

func main() {
	if len(os.Args) < 2 {
		usage()
		os.Exit(1)
	}
	switch os.Args[1] {
	case "order":
		cmdOrder(os.Args[2:])
	case "reissue":
		cmdReissue(os.Args[2:])
	case "status":
		cmdStatus(os.Args[2:])
	case "download":
		cmdDownload(os.Args[2:])
	case "-h", "--help", "help":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "不明なコマンド: %s\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}
