from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

from niannian.config import load_config, resolve_data_home
from niannian.storage.cloud_auth.baidu_oauth import (
    BaiduOAuthError,
    apply_token_response as apply_baidu_token,
    build_authorize_url as build_baidu_authorize_url,
    exchange_authorization_code as exchange_baidu_code,
)
from niannian.storage.cloud_auth.google_drive_oauth import (
    GoogleDriveOAuthError,
    apply_token_response as apply_google_token,
    build_authorize_url as build_google_authorize_url,
    exchange_authorization_code as exchange_google_code,
)
from niannian.storage.cloud_auth.onedrive_oauth import (
    OneDriveOAuthError,
    apply_token_response as apply_onedrive_token,
    build_authorize_url as build_onedrive_authorize_url,
    exchange_authorization_code as exchange_onedrive_code,
)
from niannian.storage.cloud_auth.manager import CloudCredentialManager
from niannian.storage.quark_browser_cookie import (
    audit_feimao_session,
    audit_quark_cookie,
    extract_feimao_session_from_browser,
    extract_quark_cookie_from_browser,
    read_clipboard,
)
from niannian.storage.quark_live import looks_like_quark_cookie
from niannian.storage.feimao_auth import looks_like_feimao_session


def _print_json(data: object) -> None:
    print(json.dumps(data, ensure_ascii=False, indent=2))


_OAUTH_PROVIDERS = {"baidu", "onedrive", "google_drive"}


def cmd_login_oauth(provider: str, code: str, *, home: Path, config: dict) -> int:
    if provider not in _OAUTH_PROVIDERS:
        print(f"目前 {provider} 不支持 --oauth", file=sys.stderr)
        return 1
    mgr = CloudCredentialManager(home, config=config)
    try:
        if provider == "baidu":
            token_data = exchange_baidu_code(code, config)
            apply_fn = apply_baidu_token
        elif provider == "onedrive":
            token_data = exchange_onedrive_code(code, config)
            apply_fn = apply_onedrive_token
        else:
            token_data = exchange_google_code(code, config)
            apply_fn = apply_google_token
    except (BaiduOAuthError, OneDriveOAuthError, GoogleDriveOAuthError) as exc:
        print(f"OAuth 失败: {exc}", file=sys.stderr)
        return 1

    def mutator(vault: dict) -> None:
        entry = vault.setdefault(provider, {})
        apply_fn(entry, token_data)

    from niannian.storage.cloud_auth.vault import update_vault

    update_vault(mgr.path, mutator)
    _print_json({"ok": True, "provider": provider, "status": mgr.status().get(provider)})
    return 0


def cmd_oauth_url(provider: str, *, config: dict) -> int:
    if provider not in _OAUTH_PROVIDERS:
        print(f"目前 {provider} 不支持 --oauth-url", file=sys.stderr)
        return 1
    try:
        if provider == "baidu":
            url = build_baidu_authorize_url(config)
        elif provider == "onedrive":
            url = build_onedrive_authorize_url(config)
        else:
            url = build_google_authorize_url(config)
    except (BaiduOAuthError, OneDriveOAuthError, GoogleDriveOAuthError) as exc:
        print(f"配置错误: {exc}", file=sys.stderr)
        return 1
    print(url)
    print("\n授权后复制页面上的 code，执行：")
    print(f"  niannian cloud login {provider} --oauth --code <code>")
    return 0


def cmd_login(provider: str, token: str, *, home: Path, config: dict, kind: str | None = None) -> int:
    mgr = CloudCredentialManager(home, config=config)
    mgr.set_login_token(provider, token, kind=kind)
    _print_json({"ok": True, "provider": provider, "status": mgr.status().get(provider)})
    return 0


def cmd_login_feimao(
    token: str,
    *,
    home: Path,
    config: dict,
    probe: bool = False,
) -> int:
    if not looks_like_feimao_session(token):
        print("会话格式不像飞猫（缺少 feemootoken 等）", file=sys.stderr)
        return 1
    audit = audit_feimao_session(token)
    print(
        f"飞猫会话: len={audit['len']} "
        f"feemootoken={'yes' if audit.get('feemootoken') else 'no'} "
        f"feemoo_hw_token={'yes' if audit.get('feemoo_hw_token') else 'no'}",
        file=sys.stderr,
    )
    rc = cmd_login("feimao", token, home=home, config=config, kind="session_token")
    if rc != 0 or not probe:
        return rc
    from niannian.config import load_providers_config
    from niannian.storage.cloud_base import resolve_remote_root
    from niannian.storage.feimao_api import FeimaoAPIError, FeimaoDriveClient, normalize_feimao_api_base
    from niannian.storage.feimao_http import feimao_connectivity_options

    feimao_cfg = load_providers_config().get("feimao") or {}
    api_base = normalize_feimao_api_base(feimao_cfg.get("api_base"))
    conn_opts = feimao_connectivity_options(config, feimao_cfg=feimao_cfg)
    client = FeimaoDriveClient(
        token,
        remote_root=resolve_remote_root(config, feimao_cfg),
        api_base=api_base,
        **{k: v for k, v in conn_opts.items() if v is not None},
    )
    if not client.ping():
        print("probe: fmpapi ping 失败（会话无效或网络不可达）", file=sys.stderr)
        return 1
    print("probe: fmpapi ping OK", file=sys.stderr)
    try:
        client.upload_object(".probe/cli_probe.txt", b"niannian feimao probe\n")
        print("probe: upload OK", file=sys.stderr)
    except Exception as exc:
        msg = str(exc)
        if "SSL" in msg or "timeout" in msg.lower() or "handshake" in msg.lower():
            print(
                f"probe: upload 失败（CDN/SSL）: {exc}\n"
                "提示: fmpapi 可达但上传 CDN 可能超时；可配置 storage.tier.feimao.connectivity.cdn_proxy",
                file=sys.stderr,
            )
        else:
            print(f"probe: upload 失败: {exc}", file=sys.stderr)
        return 1
    return 0


def cmd_login_quark(
    token: str,
    *,
    home: Path,
    config: dict,
    probe: bool = False,
) -> int:
    if not looks_like_quark_cookie(token):
        print("Cookie 格式不像夸克会话（缺少 __pus 等）", file=sys.stderr)
        return 1
    audit = audit_quark_cookie(token)
    print(
        f"夸克 Cookie: len={audit['len']} "
        f"__pus={'yes' if audit.get('__pus') else 'no'} "
        f"ctoken={'yes' if audit.get('ctoken') else 'no'}",
        file=sys.stderr,
    )
    if not audit.get("ctoken"):
        print("提示: 无 ctoken，live 可能不稳定；优先用 --browser 从已登录 Chrome 读取", file=sys.stderr)
    rc = cmd_login("quark", token, home=home, config=config, kind="cookie")
    if rc != 0 or not probe:
        return rc
    from niannian.storage.cloud_base import resolve_remote_root
    from niannian.storage.quark_api import QuarkAPIError, QuarkDriveClient

    try:
        client = QuarkDriveClient(token, remote_root=resolve_remote_root(config))
        client._ensure_root()
        print("probe: ensure_root OK", file=sys.stderr)
    except QuarkAPIError as exc:
        print(f"probe 失败: {exc}", file=sys.stderr)
        print(
            "请重跑配种脚本: ./scripts/quark_cloud_login.sh\n"
            "或: python3 -m niannian.cli.main cloud login quark --browser chrome --probe",
            file=sys.stderr,
        )
        return 1
    return 0


def cmd_archive_monthly(*, home: Path, config: dict, dry_run: bool) -> int:
    from niannian.config import load_providers_config
    from niannian.storage.local import LocalMemoryStore
    from niannian.storage.monthly_archive import run_monthly_archive

    store = LocalMemoryStore(home, config=config, providers=load_providers_config())
    result = run_monthly_archive(store, config=config, dry_run=dry_run)
    _print_json(result)
    return 0 if result.get("ok") or result.get("skipped_reason") else 1


def cmd_status(*, home: Path, config: dict, as_json: bool) -> int:
    mgr = CloudCredentialManager(home, config=config)
    data = mgr.status()
    if as_json:
        _print_json(data)
    else:
        for provider, info in data.items():
            auth = info.get("auth", "unknown")
            exp = info.get("expires_in_hours")
            extra = f" expires_in={exp}h" if exp is not None else ""
            print(f"{provider}: {auth}{extra}")
    return 0


def _api_client_for_resume(
    provider: str,
    *,
    home: Path,
    config: dict,
    mgr: CloudCredentialManager,
) -> Any:
    from niannian.storage.baidu_pcs import (
        BaiduPCSClient,
        DEFAULT_CHUNK_SIZE_BYTES,
        DEFAULT_MAX_UPLOAD_BYTES,
        DEFAULT_PCS_API_BASE,
        DEFAULT_PCS_SUPERFILE_BASE,
        DEFAULT_UPLOAD_MAX_BYTES,
    )
    from niannian.storage.cloud_base import resolve_remote_root
    from niannian.storage.google_drive_api import (
        DEFAULT_API_BASE,
        DEFAULT_CHUNK_SIZE_BYTES as GD_CHUNK,
        DEFAULT_MAX_UPLOAD_BYTES as GD_MAX,
        DEFAULT_UPLOAD_BASE,
        DEFAULT_UPLOAD_MAX_BYTES as GD_UPLOAD_MAX,
        GoogleDriveClient,
    )
    from niannian.storage.onedrive_api import (
        DEFAULT_CHUNK_SIZE_BYTES as OD_CHUNK,
        DEFAULT_GRAPH_BASE,
        DEFAULT_MAX_UPLOAD_BYTES as OD_MAX,
        DEFAULT_UPLOAD_MAX_BYTES as OD_UPLOAD_MAX,
        OneDriveGraphClient,
    )
    from niannian.storage.quark_api import (
        DEFAULT_CHUNK_SIZE_BYTES as Q_CHUNK,
        DEFAULT_MAX_UPLOAD_BYTES as Q_MAX,
        DEFAULT_QUARK_API_BASE,
        QuarkDriveClient,
    )

    remote_root = resolve_remote_root(config)
    cloud = (config.get("storage") or {}).get("cloud") or {}
    pc = cloud.get(provider) or {}
    sync_dir = home / "sync"

    if provider == "baidu":
        token = mgr.get_access_token("baidu")
        if not token:
            raise ValueError("baidu 未登录")
        return BaiduPCSClient(
            token,
            remote_root=remote_root,
            pcs_api_base=pc.get("pcs_api_base") or DEFAULT_PCS_API_BASE,
            pcs_superfile_base=pc.get("pcs_superfile_base") or DEFAULT_PCS_SUPERFILE_BASE,
            upload_max_bytes=int(pc.get("upload_max_bytes") or DEFAULT_UPLOAD_MAX_BYTES),
            chunk_size_bytes=int(pc.get("chunk_size_bytes") or DEFAULT_CHUNK_SIZE_BYTES),
            max_upload_bytes=int(pc.get("max_upload_bytes") or DEFAULT_MAX_UPLOAD_BYTES),
            state_path=sync_dir / "baidu_pcs_state.json",
            credential_manager=mgr,
        )
    if provider == "quark":
        cookie = mgr.get_cookie("quark") or mgr.get_refresh_token("quark")
        if not cookie:
            raise ValueError("quark 未登录")
        return QuarkDriveClient(
            cookie,
            remote_root=remote_root,
            api_base=pc.get("api_base") or DEFAULT_QUARK_API_BASE,
            state_path=sync_dir / "quark_fids.json",
            credential_manager=mgr,
            chunk_size_bytes=int(pc.get("chunk_size_bytes") or Q_CHUNK),
            max_upload_bytes=int(pc.get("max_upload_bytes") or Q_MAX),
        )
    if provider == "onedrive":
        token = mgr.get_access_token("onedrive")
        if not token:
            raise ValueError("onedrive 未登录")
        return OneDriveGraphClient(
            token,
            remote_root=remote_root,
            graph_base=pc.get("graph_base") or DEFAULT_GRAPH_BASE,
            upload_max_bytes=int(pc.get("upload_max_bytes") or OD_UPLOAD_MAX),
            chunk_size_bytes=int(pc.get("chunk_size_bytes") or OD_CHUNK),
            max_upload_bytes=int(pc.get("max_upload_bytes") or OD_MAX),
            state_path=sync_dir / "onedrive_state.json",
            credential_manager=mgr,
        )
    if provider == "google_drive":
        token = mgr.get_access_token("google_drive")
        if not token:
            raise ValueError("google_drive 未登录")
        return GoogleDriveClient(
            token,
            remote_root=remote_root,
            api_base=pc.get("api_base") or DEFAULT_API_BASE,
            upload_base=pc.get("upload_base") or DEFAULT_UPLOAD_BASE,
            upload_max_bytes=int(pc.get("upload_max_bytes") or GD_UPLOAD_MAX),
            chunk_size_bytes=int(pc.get("chunk_size_bytes") or GD_CHUNK),
            max_upload_bytes=int(pc.get("max_upload_bytes") or GD_MAX),
            state_path=sync_dir / "google_drive_state.json",
            credential_manager=mgr,
        )
    raise ValueError(f"不支持的 provider: {provider}")


def cmd_upload_checkpoints_list(
    *,
    home: Path,
    config: dict,
    provider: str | None,
    as_json: bool,
) -> int:
    from niannian.storage.upload_checkpoint import make_upload_trackers

    _, checkpoint = make_upload_trackers(home, config)
    rows = checkpoint.list_checkpoints(provider=provider)
    if as_json:
        _print_json({"checkpoints": rows, "count": len(rows)})
    else:
        if not rows:
            print("无待续传 checkpoint")
            return 0
        for row in rows:
            completed = row.get("completed_chunks") or []
            total = row.get("chunk_total") or "?"
            print(
                f"{row.get('checkpoint_id')} "
                f"{row.get('provider')} {row.get('rel_path')} "
                f"chunks={len(completed)}/{total}"
            )
    return 0


def cmd_upload_checkpoints_clear(
    *,
    home: Path,
    config: dict,
    provider: str | None,
    all_providers: bool,
) -> int:
    from niannian.storage.upload_checkpoint import make_upload_trackers

    _, checkpoint = make_upload_trackers(home, config)
    removed = checkpoint.clear(provider=provider, all_providers=all_providers)
    _print_json({"ok": True, "removed": removed})
    return 0


def cmd_upload_resume(
    rel_path: str,
    *,
    home: Path,
    config: dict,
    provider: str | None,
) -> int:
    from niannian.storage.upload_checkpoint import (
        find_checkpoint_for_rel_path,
        make_upload_trackers,
    )

    progress, checkpoint = make_upload_trackers(home, config)
    ckpt = find_checkpoint_for_rel_path(checkpoint, rel_path, provider=provider)
    use_provider = provider or (ckpt or {}).get("provider")
    if not use_provider:
        print("未找到 checkpoint，请指定 --provider", file=sys.stderr)
        return 1

    local_path = home / rel_path.strip("/")
    if not local_path.is_file():
        print(f"本地文件不存在: {local_path}", file=sys.stderr)
        return 1
    data = local_path.read_bytes()

    mgr = CloudCredentialManager(home, config=config)
    try:
        client = _api_client_for_resume(use_provider, home=home, config=config, mgr=mgr)
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 1

    try:
        client.upload_object(
            rel_path.strip("/").replace("\\", "/"),
            data,
            progress=progress,
            checkpoint=checkpoint,
        )
    except Exception as exc:
        print(f"续传失败: {exc}", file=sys.stderr)
        return 1

    active = progress.get_active()
    _print_json(
        {
            "ok": True,
            "provider": use_provider,
            "rel_path": rel_path,
            "upload_job_id": active.job_id if active else None,
        }
    )
    return 0


def run(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="niannian cloud", description="念念云盘 Vault 鉴权")
    parser.add_argument("--home", help="NIANNIAN_HOME")
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_login = sub.add_parser("login", help="登录并写入 Vault")
    p_login.add_argument("provider", choices=["quark", "baidu", "onedrive", "google_drive", "feimao"])
    p_login.add_argument("token", nargs="?", default="", help="refresh/session token")
    p_login.add_argument("--paste", action="store_true", help="从 stdin 读取 token")
    p_login.add_argument("--clipboard", action="store_true", help="从系统剪贴板读取（Mac: pbpaste）")
    p_login.add_argument(
        "--browser",
        choices=["chrome", "edge", "brave", "chromium", "firefox", "safari"],
        help="从本机浏览器 Cookie 库读取（需 pip install browser-cookie3）",
    )
    p_login.add_argument("--probe", action="store_true", help="写入 Vault 后探测 live API")
    p_login.add_argument("--cookie", action="store_true", help="夸克 Cookie；飞猫 feemootoken/Cookie")
    p_login.add_argument("--oauth", action="store_true", help="OAuth：用 authorization code 换 token")
    p_login.add_argument("--code", dest="oauth_code", default="", help="OAuth authorization code")
    p_login.add_argument("--oauth-url", action="store_true", help="打印 OAuth 授权链接")

    p_status = sub.add_parser("status", help="云盘连接态")
    p_status.add_argument("--json", action="store_true")

    p_archive = sub.add_parser("archive-monthly", help="手动触发 Google 月备")
    p_archive.add_argument("--dry-run", action="store_true")

    p_ckpt = sub.add_parser("upload-checkpoints", help="断点续传 checkpoint 管理")
    ckpt_sub = p_ckpt.add_subparsers(dest="ckpt_cmd", required=True)
    p_ckpt_list = ckpt_sub.add_parser("list", help="列出未完成 checkpoint")
    p_ckpt_list.add_argument("--provider", choices=["quark", "baidu", "onedrive", "google_drive"])
    p_ckpt_list.add_argument("--json", action="store_true")
    p_ckpt_clear = ckpt_sub.add_parser("clear", help="清除 checkpoint")
    p_ckpt_clear.add_argument("--provider", choices=["quark", "baidu", "onedrive", "google_drive"])
    p_ckpt_clear.add_argument("--all", action="store_true", dest="all_providers")

    p_resume = sub.add_parser("upload-resume", help="从 checkpoint 续传本地文件")
    p_resume.add_argument("--rel-path", required=True, help="相对 remote_root 的路径")
    p_resume.add_argument("--provider", choices=["quark", "baidu", "onedrive", "google_drive"])

    args = parser.parse_args(argv)
    home = resolve_data_home(args.home)
    config = load_config()

    if args.cmd == "login":
        if getattr(args, "oauth_url", False):
            return cmd_oauth_url(args.provider, config=config)
        if getattr(args, "oauth", False):
            code = getattr(args, "oauth_code", "") or args.token
            if getattr(args, "paste", False) or not code:
                code = sys.stdin.read().strip()
            if not code:
                print("缺少 authorization code（--code 或 stdin）", file=sys.stderr)
                return 1
            return cmd_login_oauth(args.provider, code, home=home, config=config)
        token = args.token
        browser = getattr(args, "browser", None)
        if browser and args.provider == "quark":
            try:
                token = extract_quark_cookie_from_browser(browser)
            except (RuntimeError, ValueError) as exc:
                print(str(exc), file=sys.stderr)
                return 1
        elif browser and args.provider == "feimao":
            try:
                token = extract_feimao_session_from_browser(browser)
            except (RuntimeError, ValueError) as exc:
                print(str(exc), file=sys.stderr)
                return 1
        elif browser:
            print(f"{args.provider} 不支持 --browser", file=sys.stderr)
            return 1
        elif getattr(args, "clipboard", False):
            try:
                token = read_clipboard()
            except RuntimeError as exc:
                print(str(exc), file=sys.stderr)
                return 1
        elif getattr(args, "paste", False) or not token:
            token = sys.stdin.read().strip()
        if not token:
            print("缺少 token", file=sys.stderr)
            return 1
        if args.provider == "quark" and (
            args.cookie or looks_like_quark_cookie(token) or browser
        ):
            return cmd_login_quark(
                token,
                home=home,
                config=config,
                probe=getattr(args, "probe", False),
            )
        if args.provider == "feimao" and (
            args.cookie or looks_like_feimao_session(token) or browser
        ):
            return cmd_login_feimao(
                token,
                home=home,
                config=config,
                probe=getattr(args, "probe", False),
            )
        kind = None
        if args.provider == "quark" and (args.cookie or looks_like_quark_cookie(token)):
            kind = "cookie"
        elif args.provider == "feimao" and (args.cookie or looks_like_feimao_session(token)):
            kind = "session_token"
        return cmd_login(args.provider, token, home=home, config=config, kind=kind)

    if args.cmd == "status":
        return cmd_status(home=home, config=config, as_json=args.json)

    if args.cmd == "archive-monthly":
        return cmd_archive_monthly(home=home, config=config, dry_run=getattr(args, "dry_run", False))

    if args.cmd == "upload-checkpoints":
        if args.ckpt_cmd == "list":
            return cmd_upload_checkpoints_list(
                home=home,
                config=config,
                provider=getattr(args, "provider", None),
                as_json=getattr(args, "json", False),
            )
        if args.ckpt_cmd == "clear":
            return cmd_upload_checkpoints_clear(
                home=home,
                config=config,
                provider=getattr(args, "provider", None),
                all_providers=getattr(args, "all_providers", False),
            )

    if args.cmd == "upload-resume":
        return cmd_upload_resume(
            args.rel_path,
            home=home,
            config=config,
            provider=getattr(args, "provider", None),
        )

    return 1


if __name__ == "__main__":
    raise SystemExit(run())
