from __future__ import annotations

import io
import json
import mimetypes
import os
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

import time

from lark_oapi.api.drive.v1 import (
    BatchQueryMetaRequest,
    CopyFileRequest,
    CopyFileRequestBody,
    CreateExportTaskRequest,
    CreateFolderFileRequest,
    CreateFolderFileRequestBody,
    CreatePermissionMemberRequest,
    DeleteFileRequest,
    DownloadExportTaskRequest,
    DownloadFileRequest,
    ExportTask,
    GetExportTaskRequest,
    ListFileRequest,
    Member,
    MetaRequest,
    MoveFileRequest,
    MoveFileRequestBody,
    RequestDoc,
    UploadAllFileRequest,
    UploadAllFileRequestBody,
)

from ._client import get_client, make_option
from ._scopes import InsufficientScopeError, MODULE_SCOPES

_FEISHU_OPEN_API = "https://open.feishu.cn"


def _http(method: str, path: str, token: str, *, body: Any = None, params: dict | None = None) -> dict:
    url = _FEISHU_OPEN_API + path
    if params:
        url += "?" + urllib.parse.urlencode(params)
    data = json.dumps(body).encode() if body is not None else None
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json; charset=utf-8",
    }
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as resp:
            result: dict = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        raw = e.read().decode(errors="replace")
        raise RuntimeError(f"HTTP {e.code} {path}: {raw}") from e
    if result.get("code", 0) != 0:
        code = result.get("code")
        msg = result.get("msg", "")
        if code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(f"飞书 API 错误 code={code}: {msg}")
    return result


def list_folder(
    token: str,
    folder_token: str = "",
    *,
    page_size: int = 50,
    max_count: int = 500,
) -> list:
    client = get_client()
    all_items: list = []
    page_token: str | None = None

    while len(all_items) < max_count:
        builder = ListFileRequest.builder().page_size(min(page_size, 200))
        if folder_token:
            builder = builder.folder_token(folder_token)
        if page_token:
            builder = builder.page_token(page_token)
        response = client.drive.v1.file.list(builder.build(), make_option(token))
        if not response.success():
            if response.code == 99991679:
                raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
            raise RuntimeError(
                f"列出文件夹失败: code={response.code}, msg={response.msg}"
            )
        items = response.data.files or []
        all_items.extend(items)
        if not response.data.has_more:
            break
        page_token = response.data.next_page_token

    return all_items[:max_count]


def upload(
    token: str,
    parent_token: str,
    file_path: str,
    *,
    parent_type: str = "explorer",
    file_name: str | None = None,
) -> str:
    client = get_client()
    name = file_name or os.path.basename(file_path)
    size = os.path.getsize(file_path)

    with open(file_path, "rb") as f:
        file_content = f.read()

    request = (
        UploadAllFileRequest.builder()
        .request_body(
            UploadAllFileRequestBody.builder()
            .file_name(name)
            .parent_type(parent_type)
            .parent_node(parent_token)
            .size(size)
            .file(io.BytesIO(file_content))
            .build()
        )
        .build()
    )
    response = client.drive.v1.file.upload_all(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"上传文件失败: code={response.code}, msg={response.msg}"
        )
    return response.data.file_token


def download(token: str, file_token: str, output_path: str) -> str:
    client = get_client()
    request = DownloadFileRequest.builder().file_token(file_token).build()
    response = client.drive.v1.file.download(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"下载文件失败: code={response.code}, msg={response.msg}"
        )
    with open(output_path, "wb") as f:
        f.write(response.raw.content)
    return output_path


def move(
    token: str,
    file_token: str,
    *,
    folder_token: str,
    type: str = "file",
) -> object:
    client = get_client()
    request = (
        MoveFileRequest.builder()
        .file_token(file_token)
        .request_body(
            MoveFileRequestBody.builder()
            .type(type)
            .folder_token(folder_token)
            .build()
        )
        .build()
    )
    response = client.drive.v1.file.move(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"移动文件失败: code={response.code}, msg={response.msg}"
        )
    return response.data


def copy(
    token: str,
    file_token: str,
    folder_token: str,
    name: str,
    *,
    type: str = "file",
) -> object:
    client = get_client()
    request = (
        CopyFileRequest.builder()
        .file_token(file_token)
        .request_body(
            CopyFileRequestBody.builder()
            .name(name)
            .type(type)
            .folder_token(folder_token)
            .build()
        )
        .build()
    )
    response = client.drive.v1.file.copy(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"复制文件失败: code={response.code}, msg={response.msg}"
        )
    return response.data


def delete(token: str, file_token: str, *, type: str = "file") -> None:
    client = get_client()
    request = (
        DeleteFileRequest.builder()
        .file_token(file_token)
        .type(type)
        .build()
    )
    response = client.drive.v1.file.delete(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"删除文件失败: code={response.code}, msg={response.msg}"
        )


def create_folder(token: str, parent_token: str, name: str) -> object:
    client = get_client()
    request = (
        CreateFolderFileRequest.builder()
        .request_body(
            CreateFolderFileRequestBody.builder()
            .name(name)
            .folder_token(parent_token)
            .build()
        )
        .build()
    )
    response = client.drive.v1.file.create_folder(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(
            f"创建文件夹失败: code={response.code}, msg={response.msg}"
        )
    return response.data


def get_file_meta(token: str, tokens_and_types: list[dict]) -> list[dict]:
    """Batch-fetch file metadata.

    tokens_and_types: list of {"doc_token": str, "doc_type": str}
    doc_type: "doc" | "sheet" | "bitable" | "file" | "wiki" | "docx"
    """
    client = get_client()
    request_docs = [
        RequestDoc.builder().doc_token(d["doc_token"]).doc_type(d["doc_type"]).build()
        for d in tokens_and_types
    ]
    request = (
        BatchQueryMetaRequest.builder()
        .user_id_type("open_id")
        .request_body(MetaRequest.builder().request_docs(request_docs).with_url(True).build())
        .build()
    )
    response = client.drive.v1.meta.batch_query(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(f"获取文件元信息失败: code={response.code}, msg={response.msg}")
    result = []
    for m in response.data.metas or []:
        result.append({
            "doc_token": getattr(m, "doc_token", ""),
            "doc_type": getattr(m, "doc_type", ""),
            "title": getattr(m, "title", ""),
            "owner_id": getattr(m, "owner_id", ""),
            "create_time": getattr(m, "create_time", ""),
            "latest_modify_time": getattr(m, "latest_modify_time", ""),
            "url": getattr(m, "url", ""),
        })
    return result


def search_files(
    token: str,
    query: str,
    *,
    page_size: int = 20,
    max_count: int = 100,
) -> list[dict]:
    all_items: list[dict] = []
    page_token: str | None = None
    while len(all_items) < max_count:
        body: dict = {"search_key": query, "count": min(page_size, 50)}
        if page_token:
            body["page_token"] = page_token
        result = _http("POST", "/open-apis/drive/v1/files/search", token, body=body)
        data = result.get("data", {})
        for f in data.get("files") or []:
            all_items.append({
                "token": f.get("token", ""),
                "name": f.get("name", ""),
                "type": f.get("type", ""),
                "parent_token": f.get("parent_token", ""),
                "url": f.get("url", ""),
                "created_time": f.get("created_time", ""),
                "modified_time": f.get("modified_time", ""),
                "owner_id": f.get("owner_id", ""),
            })
        if not data.get("has_more"):
            break
        page_token = data.get("next_page_token")
    return all_items[:max_count]


def set_permission(
    token: str,
    file_token: str,
    file_type: str,
    member_type: str,
    member_id: str,
    perm: str,
    *,
    need_notification: bool = False,
) -> dict:
    """Grant a permission to a member on a drive file.

    member_type: "user" | "chat" | "department" | "group"
    perm: "view" | "edit" | "full_access"
    file_type: "doc" | "sheet" | "file" | "wiki" | "bitable" | "docx"
    """
    client = get_client()
    member = Member.builder().member_type(member_type).member_id(member_id).perm(perm).build()
    request = (
        CreatePermissionMemberRequest.builder()
        .token(file_token)
        .type(file_type)
        .need_notification(need_notification)
        .request_body(member)
        .build()
    )
    response = client.drive.v1.permission_member.create(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(f"设置权限失败: code={response.code}, msg={response.msg}")
    m = response.data.member
    return {
        "member_type": getattr(m, "member_type", member_type),
        "member_id": getattr(m, "member_id", member_id),
        "perm": getattr(m, "perm", perm),
    }


def export_file(
    token: str,
    file_token: str,
    file_type: str,
    file_extension: str,
    output_path: str,
    *,
    poll_interval: float = 2.0,
    timeout: float = 120.0,
) -> str:
    """Export a Feishu cloud document to a local file.

    file_type: "doc" | "docx" | "sheet" | "bitable"
    file_extension: "xlsx" | "csv" | "docx" | "pdf"
      - sheet/bitable → xlsx, csv
      - doc/docx      → docx, pdf
    Returns the local output_path.
    """
    client = get_client()
    opt = make_option(token)

    create_request = (
        CreateExportTaskRequest.builder()
        .request_body(
            ExportTask.builder()
            .file_extension(file_extension)
            .token(file_token)
            .type(file_type)
            .build()
        )
        .build()
    )
    create_resp = client.drive.v1.export_task.create(create_request, opt)
    if not create_resp.success():
        if create_resp.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("drive", []), module="drive")
        raise RuntimeError(f"创建导出任务失败: code={create_resp.code}, msg={create_resp.msg}")
    ticket = create_resp.data.ticket

    deadline = time.monotonic() + timeout
    while True:
        if time.monotonic() > deadline:
            raise TimeoutError(f"导出任务超时（{timeout}s），ticket={ticket}")
        get_request = (
            GetExportTaskRequest.builder()
            .ticket(ticket)
            .token(file_token)
            .build()
        )
        get_resp = client.drive.v1.export_task.get(get_request, opt)
        if not get_resp.success():
            raise RuntimeError(f"查询导出任务失败: code={get_resp.code}, msg={get_resp.msg}")
        result = get_resp.data.result
        job_status = getattr(result, "job_status", -1)
        if job_status == 0:
            export_file_token = getattr(result, "file_token", "")
            break
        if job_status not in (0, 1, 2):
            raise RuntimeError(f"导出失败: {getattr(result, 'job_error_msg', job_status)}")
        time.sleep(poll_interval)

    dl_request = DownloadExportTaskRequest.builder().file_token(export_file_token).build()
    dl_resp = client.drive.v1.export_task.download(dl_request, opt)
    if not dl_resp.success():
        raise RuntimeError(f"下载导出文件失败: code={dl_resp.code}, msg={dl_resp.msg}")
    with open(output_path, "wb") as f:
        f.write(dl_resp.file.read())
    return output_path
