from __future__ import annotations

import asyncio
import hashlib
from datetime import datetime

import requests

from appstore.models import AppRecord, DebPackageInfo, ReleaseRecord, UploadedFileRef
from appstore.platforms import resolve_store_arch, resolve_store_platform
from appstore.pyppeteer_runtime import PYPPETEER_LAUNCH_OPTIONS, launch
from appstore.update_payload import (
    baseline_id_objects,
    baseline_system_id_objects,
    build_reused_basic_info,
    build_reused_fit_info,
    build_reused_lan_infos,
    merge_origin_pkgs,
    normalize_website_for_submit,
)

ORIGIN = "https://appstore-dev.uniontech.com"
BASE = f"{ORIGIN}/devprod-api"
REQUEST_TIMEOUT = 120
UPLOAD_PUT_TIMEOUT = 900
UPLOAD_PUT_ATTEMPTS = 3


class AppLookupAmbiguousError(RuntimeError):
    pass


class PayloadBuildError(ValueError):
    pass


class AuthenticationError(RuntimeError):
    pass


class AppStoreProtocolError(RuntimeError):
    pass


def package_family_from_install_mode(value: object) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    try:
        numeric = str(int(float(text)))
    except (TypeError, ValueError):
        numeric = text.lower()
    if numeric == "2":
        return "linglong"
    if numeric == "1":
        return "deb"
    return ""


def _package_family_from_text(value: object) -> str:
    text = str(value or "").strip().lower()
    if not text:
        return ""
    if text in {"linglong", "玲珑", "uab", "layer"} or "玲珑" in text or "linglong" in text:
        return "linglong"
    if text == "22":
        return "linglong"
    if text in {"deb", "debian", "11"} or "deb" in text:
        return "deb"
    return ""


def _package_format_from_text(value: object) -> str:
    text = str(value or "").strip().lower()
    if not text:
        return ""
    if text in {"deb", "uab", "layer"}:
        return text
    if text == "11" or "deb" in text:
        return "deb"
    if "layer" in text:
        return "layer"
    if text == "22" or "uab" in text or "玲珑" in text or "linglong" in text:
        return "uab"
    return ""


def package_family_from_store_row(row: dict) -> str:
    for key in ("package_family", "pkg_family", "family", "packageFamily"):
        family = _package_family_from_text(row.get(key))
        if family:
            return family
    for key in (
        "pkgInstallMode",
        "pkg_install_mode",
        "package_install_mode",
        "packageInstallMode",
        "install_mode",
    ):
        family = package_family_from_install_mode(row.get(key))
        if family:
            return family
    for key in (
        "pkgType",
        "pkg_type",
        "package_type",
        "package_format",
        "pkg_format",
        "format",
        "type",
    ):
        family = _package_family_from_text(row.get(key))
        if family:
            return family
    return ""


def package_format_from_store_row(row: dict, package_family: str = "") -> str:
    for key in ("package_format", "pkg_format", "format", "pkgType", "pkg_type", "package_type", "type"):
        package_format = _package_format_from_text(row.get(key))
        if package_format:
            return package_format
    family = (package_family or package_family_from_store_row(row)).strip()
    if family == "deb":
        return "deb"
    if family == "linglong":
        return "uab"
    return ""


def _detail_data(detail: dict) -> dict:
    datas = detail.get("datas") if isinstance(detail, dict) else None
    return datas if isinstance(datas, dict) else detail


def package_family_from_app_detail(detail: dict) -> str:
    data = _detail_data(detail)
    basic_info = data.get("app_basic_info") if isinstance(data, dict) else {}
    if isinstance(basic_info, dict):
        family = package_family_from_install_mode(basic_info.get("pkgInstallMode"))
        if family:
            return family
        for key in ("package_family", "pkg_family", "pkgType", "pkg_type", "package_type"):
            family = _package_family_from_text(basic_info.get(key))
            if family:
                return family
    origin_pkgs = data.get("app_origin_pkgs") if isinstance(data, dict) else []
    for origin_pkg in origin_pkgs if isinstance(origin_pkgs, list) else []:
        if not isinstance(origin_pkg, dict):
            continue
        for key in ("pkgType", "pkg_type", "package_type", "package_format", "pkg_format"):
            family = _package_family_from_text(origin_pkg.get(key))
            if family:
                return family
    return ""


def package_format_from_app_detail(detail: dict, package_family: str = "") -> str:
    data = _detail_data(detail)
    basic_info = data.get("app_basic_info") if isinstance(data, dict) else {}
    if isinstance(basic_info, dict):
        for key in ("package_format", "pkg_format", "format", "pkgType", "pkg_type", "package_type"):
            package_format = _package_format_from_text(basic_info.get(key))
            if package_format:
                return package_format
    origin_pkgs = data.get("app_origin_pkgs") if isinstance(data, dict) else []
    for origin_pkg in origin_pkgs if isinstance(origin_pkgs, list) else []:
        if not isinstance(origin_pkg, dict):
            continue
        for key in ("package_format", "pkg_format", "format", "pkgType", "pkg_type", "package_type"):
            package_format = _package_format_from_text(origin_pkg.get(key))
            if package_format:
                return package_format
    family = (package_family or package_family_from_app_detail(detail)).strip()
    if family == "deb":
        return "deb"
    if family == "linglong":
        return "uab"
    return ""


def choose_target_app_id(matches: list[dict], override: str) -> str:
    if override:
        return override
    if not matches:
        return ""
    if len(matches) > 1:
        raise AppLookupAmbiguousError("multiple app matches found; set app_id_override")
    return str(matches[0]["app_id"])


def _upload_time_label() -> str:
    now = datetime.now()
    return f"{now.year}-{now.month}-{now.day}  {now.hour}:{now.minute}:{now.second}  "


def _resolve_region_code(region: str) -> int:
    normalized = region.strip() if region else "1"
    try:
        return int(normalized)
    except ValueError as exc:
        raise PayloadBuildError(f"invalid region value: {region}") from exc


def _resolve_baseline_values(baseline: str) -> list[str]:
    normalized = baseline.strip()
    if not normalized:
        return []
    return [normalized]


def _adapt_code_objects(codes: tuple[str, ...] | list[str] | None) -> list[dict]:
    if codes is None:
        return []
    result: list[dict] = []
    seen: set[str] = set()
    for code in codes:
        normalized = str(code).strip()
        if not normalized or normalized in seen:
            continue
        seen.add(normalized)
        result.append({"code": int(normalized) if normalized.isdigit() else normalized})
    return result


def _pick_auth_headers(*, local_storage: dict | None = None, session_storage: dict | None = None) -> tuple[str, str]:
    storage_chain = (session_storage or {}, local_storage or {})
    direct_authorization = ("authorization", "authorizationtoken", "auth", "authtoken")
    access_token_keys = ("access_token", "accesstoken", "token", "id_token", "idtoken")
    refresh_markers = ("refresh",)

    authorization_value = ""
    token_value = ""
    for storage in storage_chain:
        for key, value in storage.items():
            if not value:
                continue
            normalized_key = str(key).replace("-", "").replace("_", "").lower()
            if any(marker in normalized_key for marker in refresh_markers):
                continue
            if not authorization_value and normalized_key in direct_authorization:
                authorization_value = value
            if not token_value and normalized_key in access_token_keys:
                token_value = value
        if authorization_value and token_value:
            break

    if not authorization_value:
        authorization_value = token_value
    if not token_value:
        token_value = authorization_value
    return authorization_value, token_value


def _pick_auth_cookie(cookies: list[dict] | None = None) -> str:
    for cookie in cookies or []:
        normalized_name = str(cookie.get("name", "")).replace("-", "").replace("_", "").lower()
        if normalized_name in {"accesstoken", "authorization", "token"}:
            value = str(cookie.get("value", "")).strip()
            if value:
                return value
    return ""


def _normalize_app_list_payload(payload: dict | list, action: str) -> list[dict]:
    if isinstance(payload, list):
        return payload
    rows = payload.get("rows")
    if isinstance(rows, list):
        return rows
    datas = payload.get("datas", payload)
    if isinstance(datas, list):
        return datas
    if isinstance(datas, dict):
        for key in ("list", "records", "rows"):
            value = datas.get(key)
            if isinstance(value, list):
                return value
    raise AppStoreProtocolError(f"{action} returned unexpected payload: {payload}")


def _normalize_paged_rows_payload(payload: dict | list, action: str) -> dict:
    if isinstance(payload, list):
        return {"total": len(payload), "rows": payload}
    if not isinstance(payload, dict):
        raise AppStoreProtocolError(f"{action} returned unexpected payload: {payload}")
    rows = payload.get("rows")
    total = payload.get("total", 0)
    if isinstance(rows, list):
        return {"total": int(total or len(rows)), "rows": rows}
    datas = payload.get("datas")
    if isinstance(datas, dict):
        rows = datas.get("rows") or datas.get("list") or datas.get("records")
        total = datas.get("total", total)
        if isinstance(rows, list):
            return {"total": int(total or len(rows)), "rows": rows}
    raise AppStoreProtocolError(f"{action} returned unexpected payload: {payload}")


def _response_error_detail(response) -> str:
    try:
        payload = response.json()
    except Exception:
        payload = None
    if isinstance(payload, (dict, list)):
        return str(payload)
    text = str(getattr(response, "text", "") or "").strip()
    return text[:500]


def build_submit_payload(
    *,
    app: AppRecord,
    release: ReleaseRecord,
    package_info: DebPackageInfo,
    uploads: dict[str, UploadedFileRef | tuple[UploadedFileRef, ...]],
    target_app_id: str,
    existing_app_detail: dict | None = None,
    existing_app_overrides: dict[str, object] | None = None,
    localized_lan_texts: dict[str, dict[str, str]] | None = None,
    desired_lans: tuple[str, ...] | None = None,
) -> dict:
    platform = resolve_store_platform(release.system_platform)
    release_arch = resolve_store_arch(release.arch)
    package_arch = resolve_store_arch(package_info.pkg_arch)
    if release_arch.code != package_arch.code:
        raise PayloadBuildError(
            f"arch mismatch between release '{release.arch}' and package '{package_info.pkg_arch}'"
        )
    arch = package_arch
    region_code = _resolve_region_code(release.region)
    baseline_values = _resolve_baseline_values(release.baseline)
    cpu_clip_codes = getattr(release, "cpu_clip_codes", None)
    motherboard_codes = getattr(release, "motherboard_codes", None)
    package_upload = uploads["package"]
    package_install_mode = 1
    origin_pkg = {
        "pkg_name": package_info.pkg_name,
        "pkg_version": package_info.pkg_version,
        "pkg_arch": arch.code,
        "pkgArch": arch.label,
        "pkg_mode": 0,
        "pkgChannel": None,
        "pkgType": platform.pkg_type,
        "progressPercent": 100,
        "sums": 0,
        "index": 0,
        "pkg_size": package_info.pkg_size,
        "sha256": package_info.sha256,
        "upload_time": _upload_time_label(),
        "file_save_key": package_upload.file_save_key,
        "system_platform": [platform.code],
        "systemStr": platform.system_label,
        "supSys": platform.sup_sys,
        "baseline": baseline_system_id_objects([str(platform.code)], baseline_values),
        "unsupportBaseline": [],
        "supBlineVer": release.baseline.strip(),
        "unsupportBlineVers": "",
    }

    if existing_app_detail is not None:
        overrides = existing_app_overrides or {}
        app_info = {
            "app_lan_infos": build_reused_lan_infos(
                existing_app_detail,
                release_note=release.note,
                screenshot_uploads=(
                    tuple(uploads["screenshots"])
                    if "screenshots" in uploads
                    else None
                ),
                icon_upload=uploads["icon"] if "icon" in uploads else None,
                name=(
                    str(overrides["app_name_zh"]).strip()
                    if "app_name_zh" in overrides and overrides["app_name_zh"] is not None
                    else None
                ),
                brief_info=(
                    str(overrides["short_desc_zh"]).strip()
                    if "short_desc_zh" in overrides and overrides["short_desc_zh"] is not None
                    else None
                ),
                desc_info=(
                    str(overrides["full_desc_zh"]).strip()
                    if "full_desc_zh" in overrides and overrides["full_desc_zh"] is not None
                    else None
                ),
                localized_texts=localized_lan_texts,
                desired_lans=desired_lans,
            ),
            "app_basic_info": build_reused_basic_info(
                existing_app_detail,
                package_install_mode=package_install_mode,
                region=str(region_code),
                category_id=(
                    int(overrides["category_id"])
                    if "category_id" in overrides and overrides["category_id"] is not None
                    else None
                ),
                website=(
                    str(overrides["website"]).strip()
                    if "website" in overrides and overrides["website"] is not None
                    else None
                ),
            ),
            "app_fit_info": build_reused_fit_info(
                existing_app_detail,
                fit_system_codes=[str(platform.code)],
                fit_baseline_ids=baseline_values,
                fit_unsupported_ids=[],
                fit_arch_codes=[str(arch.code)],
                region_codes=[region_code],
                fit_cpu_clip_codes=list(cpu_clip_codes) if cpu_clip_codes is not None else None,
                fit_motherboard_codes=list(motherboard_codes) if motherboard_codes is not None else None,
            ),
            "app_origin_pkgs": merge_origin_pkgs(existing_app_detail, [origin_pkg]),
        }
    else:
        screenshots = uploads["screenshots"]
        icon_upload = uploads["icon"]
        app_info = {
            "app_lan_infos": build_reused_lan_infos(
                None,
                release_note=release.note,
                screenshot_uploads=tuple(screenshots),
                icon_upload=icon_upload,
                localized_texts=localized_lan_texts
                or {
                    "zh_CN": {
                        "name": app.app_name_zh,
                        "brief_info": app.short_desc_zh,
                        "desc_info": app.full_desc_zh,
                    }
                },
                desired_lans=desired_lans or ("zh_CN",),
            ),
            "app_basic_info": {
                "default_lan": "zh_CN",
                "pkg_mode": 0,
                "pkgInstallMode": package_install_mode,
                "inAppPayment": 0,
                "category_id": app.category_id,
                "website": normalize_website_for_submit(app.website),
                "region": str(region_code),
            },
            "app_fit_info": {
                "system_mode": [{"code": 1}],
                "baseline": baseline_id_objects(baseline_values),
                "unsupportBaseline": [],
                "system_platform": [{"code": platform.code}],
                "region": [{"code": region_code}],
                "arch": [{"code": arch.code}],
                "cpu_clip": _adapt_code_objects(cpu_clip_codes),
                "motherboard": _adapt_code_objects(motherboard_codes),
                "supWayland": 0,
            },
            "app_origin_pkgs": [origin_pkg],
        }

    payload = {
        "operate_type": 52,
        "app_info": app_info,
    }
    if target_app_id:
        payload["app_id"] = target_app_id
        if existing_app_detail is not None and not app_info["app_basic_info"].get("app_id"):
            app_info["app_basic_info"]["app_id"] = target_app_id
    return payload


def build_requests_session(
    *,
    cookies: list[dict] | None = None,
    local_storage: dict | None = None,
    session_storage: dict | None = None,
) -> requests.Session:
    session = requests.Session()
    session.headers.update(
        {
            "Origin": ORIGIN,
            "Referer": f"{ORIGIN}/",
            "User-Agent": "Mozilla/5.0",
        }
    )
    for cookie in cookies or []:
        session.cookies.set(
            cookie["name"],
            cookie["value"],
            domain=cookie.get("domain"),
            path=cookie.get("path", "/"),
        )

    authorization_value, token_value = _pick_auth_headers(
        local_storage=local_storage,
        session_storage=session_storage,
    )
    cookie_token_value = _pick_auth_cookie(cookies)
    if not authorization_value:
        authorization_value = cookie_token_value
    if not token_value:
        token_value = cookie_token_value

    if authorization_value:
        session.headers["Authorization"] = authorization_value
    elif token_value:
        session.headers["Authorization"] = token_value
    if token_value:
        session.headers["token"] = token_value
    elif authorization_value:
        session.headers["token"] = authorization_value
    return session


class AppStoreClient:
    def __init__(self, session: requests.Session | None = None) -> None:
        self.session = session or requests.Session()

    @staticmethod
    def _ensure_ok(response, action: str) -> dict | list:
        status_code = getattr(response, "status_code", 200)
        if status_code >= 400:
            detail = _response_error_detail(response)
            if detail:
                raise RuntimeError(f"{action} failed with HTTP {status_code}: {detail}")
            raise RuntimeError(f"{action} failed with HTTP {status_code}")
        payload = response.json()
        if isinstance(payload, list):
            return payload
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"{action} returned unexpected payload: {payload}")
        status = payload.get("status")
        if status not in (None, 200):
            raise RuntimeError(f"{action} failed: {payload}")
        return payload

    async def _login_and_export_state(self, username: str, password: str) -> tuple[list[dict], dict, dict]:
        browser = await launch(headless=True, **PYPPETEER_LAUNCH_OPTIONS)
        try:
            page = await browser.newPage()
            await page.goto(f"{ORIGIN}/#/index", {"waitUntil": "networkidle2", "timeout": 120000})
            await page.waitForSelector('input[type="text"]', {"timeout": 120000})
            inputs = await page.querySelectorAll("input")
            if len(inputs) < 2:
                raise AuthenticationError("login page did not expose username/password inputs")
            await inputs[0].type(username, {"delay": 30})
            await inputs[1].type(password, {"delay": 30})
            await page.click("button")
            await page.waitForNavigation({"waitUntil": "networkidle2", "timeout": 120000})
            cookies = await page.cookies()
            for _ in range(15):
                if _pick_auth_cookie(cookies):
                    break
                await asyncio.sleep(1)
                cookies = await page.cookies()
            local_storage = await page.evaluate(
                """() => {
                  const out = {};
                  for (let i = 0; i < localStorage.length; i += 1) {
                    const key = localStorage.key(i);
                    out[key] = localStorage.getItem(key);
                  }
                  return out;
                }"""
            )
            session_storage = await page.evaluate(
                """() => {
                  const out = {};
                  for (let i = 0; i < sessionStorage.length; i += 1) {
                    const key = sessionStorage.key(i);
                    out[key] = sessionStorage.getItem(key);
                  }
                  return out;
                }"""
            )
            return cookies, local_storage, session_storage
        finally:
            await browser.close()

    def login(self, username: str, password: str) -> None:
        cookies, local_storage, session_storage = asyncio.run(self._login_and_export_state(username, password))
        self.session = build_requests_session(
            cookies=cookies,
            local_storage=local_storage,
            session_storage=session_storage,
        )
        try:
            self._ensure_ok(self.session.get(f"{BASE}/store-dev-auth/dev_info", timeout=REQUEST_TIMEOUT), "dev_info")
        except Exception as exc:
            raise AuthenticationError("authentication verification failed") from exc

    def fetch_linglong_system_lines(self) -> list[dict]:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/system/dict/data/type/linglong_app_sup_sys",
                timeout=REQUEST_TIMEOUT,
            ),
            "fetch_linglong_system_lines",
        )
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"fetch_linglong_system_lines returned unexpected payload: {payload}")
        rows = payload.get("datas", [])
        if not isinstance(rows, list):
            raise AppStoreProtocolError(f"fetch_linglong_system_lines returned unexpected payload: {payload}")
        return list(rows)

    def fetch_adapt_info(self) -> dict:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/adapt-info/",
                timeout=REQUEST_TIMEOUT,
            ),
            "fetch_adapt_info",
        )
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"fetch_adapt_info returned unexpected payload: {payload}")
        return payload

    def fetch_categories(self) -> list[dict]:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/category",
                timeout=REQUEST_TIMEOUT,
            ),
            "fetch_categories",
        )
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"fetch_categories returned unexpected payload: {payload}")
        rows = payload.get("datas", [])
        if not isinstance(rows, list):
            raise AppStoreProtocolError(f"fetch_categories returned unexpected payload: {payload}")
        return list(rows)

    def fetch_dev_info(self) -> dict:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-auth/dev_info",
                timeout=REQUEST_TIMEOUT,
            ),
            "fetch_dev_info",
        )
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"fetch_dev_info returned unexpected payload: {payload}")
        datas = payload.get("datas")
        if not isinstance(datas, dict):
            raise AppStoreProtocolError(f"fetch_dev_info returned unexpected payload: {payload}")
        return datas

    def list_apps(self, page_num: int = 1, page_size: int = 200) -> list[dict]:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/app",
                params={"pageNum": page_num, "pageSize": page_size},
                timeout=REQUEST_TIMEOUT,
            ),
            "list_apps",
        )
        return _normalize_app_list_payload(payload, "list_apps")

    def list_apps_paged(self, page_num: int = 1, page_size: int = 50, **filters: object) -> dict:
        params = {"pageNum": page_num, "pageSize": page_size}
        params.update({key: value for key, value in filters.items() if value not in (None, "")})
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/app",
                params=params,
                timeout=REQUEST_TIMEOUT,
            ),
            "list_apps_paged",
        )
        return _normalize_paged_rows_payload(payload, "list_apps_paged")

    def report_my_apps(self) -> dict:
        payload = self._ensure_ok(
            self.session.get(f"{BASE}/store-dev-app/report/my-apps", timeout=REQUEST_TIMEOUT),
            "report_my_apps",
        )
        if not isinstance(payload, dict) or not isinstance(payload.get("datas"), dict):
            raise AppStoreProtocolError(f"report_my_apps returned unexpected payload: {payload}")
        return payload["datas"]

    def report_my_downcounts(self) -> dict:
        payload = self._ensure_ok(
            self.session.get(f"{BASE}/store-dev-app/report/my-downcounts", timeout=REQUEST_TIMEOUT),
            "report_my_downcounts",
        )
        if not isinstance(payload, dict) or not isinstance(payload.get("datas"), dict):
            raise AppStoreProtocolError(f"report_my_downcounts returned unexpected payload: {payload}")
        return payload["datas"]

    def report_my_comments(self) -> dict:
        payload = self._ensure_ok(
            self.session.get(f"{BASE}/store-dev-app/report/my-comments", timeout=REQUEST_TIMEOUT),
            "report_my_comments",
        )
        if not isinstance(payload, dict) or not isinstance(payload.get("datas"), dict):
            raise AppStoreProtocolError(f"report_my_comments returned unexpected payload: {payload}")
        return payload["datas"]

    def get_app_history(self, app_id: str, page_num: int = 1, page_size: int = 10) -> dict:
        normalized_app_id = str(app_id).strip()
        if not normalized_app_id:
            raise ValueError("app_id is required")
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/app/{normalized_app_id}/history",
                params={"pageNum": page_num, "pageSize": page_size},
                timeout=REQUEST_TIMEOUT,
            ),
            "get_app_history",
        )
        return _normalize_paged_rows_payload(payload, "get_app_history")

    def get_app_audit_status(self, detail_id: str) -> dict:
        normalized_detail_id = str(detail_id).strip()
        if not normalized_detail_id:
            raise ValueError("detail_id is required")
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/app/{normalized_detail_id}/audit_status",
                timeout=REQUEST_TIMEOUT,
            ),
            "get_app_audit_status",
        )
        if not isinstance(payload, dict) or not isinstance(payload.get("datas"), dict):
            raise AppStoreProtocolError(f"get_app_audit_status returned unexpected payload: {payload}")
        return payload["datas"]

    def find_apps_by_pkg_name(self, pkg_name: str) -> list[dict]:
        normalized = pkg_name.strip()
        matches: list[dict] = []
        page_num = 1
        page_size = 200
        while True:
            page = self.list_apps(page_num=page_num, page_size=page_size)
            if not page:
                return matches
            matches.extend(row for row in page if str(row.get("pkg_name", "")).strip() == normalized)
            page_num += 1

    def get_app_detail(self, detail_id: str) -> dict:
        payload = self._ensure_ok(
            self.session.get(
                f"{BASE}/store-dev-app/app/{detail_id}/detail",
                timeout=REQUEST_TIMEOUT,
            ),
            "get_app_detail",
        )
        if not isinstance(payload, dict):
            raise AppStoreProtocolError(f"get_app_detail returned unexpected payload: {payload}")
        datas = payload.get("datas")
        if not isinstance(datas, dict):
            raise AppStoreProtocolError(f"get_app_detail returned unexpected payload: {payload}")
        return datas

    def submit_payload(self, payload: dict) -> dict:
        response = self.session.post(
            f"{BASE}/store-dev-app/app",
            json=payload,
            timeout=REQUEST_TIMEOUT,
        )
        validated = self._ensure_ok(response, "submit_app")
        if not isinstance(validated, dict):
            raise AppStoreProtocolError(f"submit_app returned unexpected payload: {validated}")
        return validated

    def upload_file_bytes(self, filename: str, data: bytes, upload_type: str) -> UploadedFileRef:
        file_hash = hashlib.md5(data).hexdigest()
        begin_payload = {
            "fileName": filename,
            "uploadType": upload_type,
            "size": len(data),
            "md5": file_hash,
            "chunks": [{"hash": file_hash, "size": len(data)}],
        }
        begin = self._ensure_ok(
            self.session.post(f"{BASE}/store-file/upload/begin", json=begin_payload, timeout=REQUEST_TIMEOUT),
            "upload_begin",
        )
        info = begin.get("datas") or {}
        upload_url = (
            info.get("uploadUrl")
            or info.get("upload_url")
            or (((info.get("upload_infos") or [{}])[0]).get("upload_url"))
        )
        file_save_key = info.get("fileSaveKey") or info.get("file_save_key")
        file_upload_record_id = info.get("id") or info.get("fileUploadRecordId") or info.get("file_upload_record_id")
        upload_id = info.get("uploadId") if "uploadId" in info else info.get("upload_id")
        if not upload_url or not file_save_key:
            raise RuntimeError(f"upload_begin failed: unexpected response payload {begin}")
        put_response = None
        last_put_error: Exception | None = None
        for _attempt in range(UPLOAD_PUT_ATTEMPTS):
            try:
                put_response = requests.put(
                    upload_url,
                    data=data,
                    headers={"Content-Type": "application/octet-stream"},
                    timeout=UPLOAD_PUT_TIMEOUT,
                )
                last_put_error = None
                break
            except requests.RequestException as exc:
                last_put_error = exc
        if last_put_error is not None:
            raise last_put_error
        if hasattr(put_response, "raise_for_status"):
            put_response.raise_for_status()
        elif getattr(put_response, "status_code", 200) >= 400:
            raise RuntimeError(f"upload_put failed with HTTP {put_response.status_code}")

        end_payload = {
            "chunks": [file_hash],
            "hash": file_hash,
            "file_upload_record_id": file_upload_record_id,
            "size": len(data),
            "upload_id": upload_id,
            "file_save_key": file_save_key,
            "status": 2,
        }
        self._ensure_ok(
            self.session.post(f"{BASE}/store-file/upload/end", json=end_payload, timeout=REQUEST_TIMEOUT),
            "upload_end",
        )
        return UploadedFileRef(kind=upload_type, file_save_key=file_save_key, size=len(data), file_hash=file_hash)
