from __future__ import annotations

import os
import time
from collections import deque
from enum import Enum
from typing import Any, List, Dict
from pathlib import PurePosixPath
import logging

from loki.common.profile import Profile, ProfileManager
from loki.scrapers.base import Method, RequestMixin

logger = logging.getLogger('loki')


class PathExpect(str, Enum):
    ANY = "any"
    DIR = "dir"
    FILE = "file"


class Pan115(RequestMixin):
    """
    不支持文件夹名称包含"/"
    """
    
    PLATFORM = "115"

    def __init__(
        self,
        *,
        profile: Profile | None = None,
        profile_name: str = "default",
        profile_manager: ProfileManager | None = None,
        auto_load_cookies: bool = True,
    ) -> None:
        if profile is None:
            manager = profile_manager or ProfileManager()
            profile = manager.get(self.PLATFORM, profile_name)

        super().__init__(profile=profile, auto_load_cookies=auto_load_cookies)
        config = profile.read_config()
        self.uid = str(config.get("uid", ""))
        if not self.uid:
            self.uid = self.get_uid()
            config['uid'] = self.uid
            profile.write_config(config=config)
        
    def get_uid(self) -> str:
        url = 'https://webapi.115.com/files'
        params = {
            'aid': '1',
            'cid': '0',
            'offset': '0',
            'limit': '20',
            'type': '0',
            'show_dir': '1',
            'fc_mix': '0',
            'natsort': '1',
            'count_folders': '1',
            'format': 'json',
            'custom_order': '0'
        }
        resp = self.request(Method.GET, url, params=params)
        if resp:
            data = resp.json()
            if data['state'] == True:
                return data['uid']
        return '' 
           
    def path_to_id(self, pan_path: str, expect: PathExpect = PathExpect.ANY) -> str:
        """
        开头有无/都视为绝对路径
        """     
        if not isinstance(expect, PathExpect):
            raise TypeError(f"expect must be PathExpect, got {type(expect).__name__}")
        id = '0'
        cur = PurePosixPath('/')
        p = '/' / PurePosixPath(pan_path)
        if len(p.parts) == 1:
            return id if expect in (PathExpect.ANY, PathExpect.DIR) else ""
        for part in p.parts[1:]:
            cur /= part
            is_last = (cur == p)
            children = self.list_files(id)
            dirs = [c for c in children if c.get('name') == cur.name and bool(c.get('is_dir', False))]
            files = [c for c in children if c.get('name') == cur.name and not bool(c.get('is_dir', False))]

            if not is_last:
                if not dirs:
                    return ''
                next_id = str(dirs[0].get('id', '') or '').strip()
                if not next_id:
                    return ''
                id = next_id
                continue

            if expect == PathExpect.DIR:
                chosen = dirs[0] if dirs else None
            elif expect == PathExpect.FILE:
                chosen = files[0] if files else None
            else:
                chosen = (dirs[0] if dirs else None) or (files[0] if files else None)
            if not chosen:
                return ""
            next_id = str(chosen.get('id', '') or '').strip()
            if not next_id:
                return ""
            id = next_id
        return id
    
    def paths_to_ids(
        self,
        paths: List[str],
        limit: str = '20',
        expect: PathExpect = PathExpect.ANY,
        sleep_seconds: float = 0.7,
    ) -> List[str]:
        """
        缓存遍历过的文件、文件夹，减少请求调用
        开头有无/都视为绝对路径
        不存在文件/文件夹 或 类型不匹配(expect) 的 id 为 -1
        """
        if not isinstance(expect, PathExpect):
            raise TypeError(f"expect must be PathExpect, got {type(expect).__name__}")
        path2meta_cache: Dict[str, Dict[str, str]] = {'/': {'dir': '0'}}
        dirid2files_cache: Dict[str, List[Dict[str, Any]]] = {}
        results = []
        
        for p_str in paths:
            p = '/' / PurePosixPath(p_str)
            p_posix = p.as_posix()
            cached = path2meta_cache.get(p_posix)
            if cached:
                if expect == PathExpect.DIR:
                    results.append(cached.get('dir', '') or '-1')
                elif expect == PathExpect.FILE:
                    results.append(cached.get('file', '') or '-1')
                else:
                    results.append(cached.get('dir', '') or cached.get('file', '') or '-1')
                continue
            
            cur = PurePosixPath('/')
            for part in p.parts[1:]:
                parent_meta = path2meta_cache.get(cur.as_posix())
                parent_dir_id = str((parent_meta or {}).get('dir', '') or '').strip()
                if not parent_dir_id:
                    break

                children = dirid2files_cache.get(parent_dir_id)
                if children is None:
                    if sleep_seconds > 0:
                        time.sleep(sleep_seconds)
                    children = self.list_files(parent_dir_id, limit)
                    dirid2files_cache[parent_dir_id] = children

                for child in children:
                    name = str(child.get('name', '') or '')
                    if not name:
                        continue
                    child_id = str(child.get('id', '') or '').strip()
                    if not child_id:
                        continue
                    child_path = (cur / name).as_posix()
                    meta = path2meta_cache.setdefault(child_path, {})
                    if bool(child.get('is_dir', False)):
                        meta['dir'] = child_id
                    else:
                        meta['file'] = child_id
                cur /= part
            resolved = path2meta_cache.get(p_posix)
            if not resolved:
                results.append('-1')
            elif expect == PathExpect.DIR:
                results.append(resolved.get('dir', '') or '-1')
            elif expect == PathExpect.FILE:
                results.append(resolved.get('file', '') or '-1')
            else:
                results.append(resolved.get('dir', '') or resolved.get('file', '') or '-1')
        
        return results
            
    def list_files(self, id: str = '0', limit: str = '20') -> List[Dict[str, Any]]:
        items: List[Dict[str, Any]] = []
        
        url = 'https://webapi.115.com/files'
        params = {
            'aid': '1',
            'cid': id,
            'offset': '0',
            'limit': limit,
            'type': '0',
            'show_dir': '1',
            'fc_mix': '0',
            'natsort': '1',
            'count_folders': '1',
            'format': 'json',
            'custom_order': '0'
        }
        resp = self.request(Method.GET, url, params=params)
        if resp:
            data = resp.json()
            if data['state'] == True and str(data['cid']) == id:
                for f in data['data']:
                    name = str(f.get('n', '') or '')
                    if not name:
                        continue

                    fid = str(f.get('fid', '') or '').strip()
                    if fid:
                        items.append({
                            'id': fid,
                            'name': name,
                            'size': int(f.get('s', 0)),
                            'class': str(f.get('class', '')),
                            'is_dir': False,
                        })
                        continue

                    # Folder name may contain suffix like "abc.mp4", so do not infer by extension.
                    cid = str(f.get('cid', '') or '').strip()
                    if not cid:
                        continue
                    items.append({
                        'id': cid,
                        'name': name,
                        'size': int(f.get('s', 0)),
                        'class': '',
                        'is_dir': True,
                    })
                
        return items

    def walk_path(self, pan_path: str, limit: str = '1000', sleep_seconds: float = 1) -> List[Dict[str, Any]]:
        """
        递归遍历指定目录下所有文件/文件夹。

        返回结果结构与 `list_files` 一致，并额外附带 `path` 字段（绝对路径）。

        注意：底层仍依赖 `list_files` 的 `limit`，目录项超过 limit 时结果可能不完整；
        且同一目录下如果存在同名“文件+文件夹”，会同时返回两条记录。
        """
        base = '/' / PurePosixPath(pan_path)
        base_id = self.path_to_id(base.as_posix(), expect=PathExpect.DIR)
        if not base_id:
            return []

        results: List[Dict[str, Any]] = []
        q = deque([(base, base_id)])
        first_request = True

        while q:
            cur_path, cur_id = q.popleft()
            if not first_request and sleep_seconds > 0:
                time.sleep(sleep_seconds)
            first_request = False
            children = self.list_files(cur_id, limit)
            for info in children:
                name = str(info.get('name', '') or '')
                if not name:
                    continue
                child_path = (cur_path / name).as_posix()
                entry = dict(info)
                entry['path'] = child_path
                results.append(entry)

                child_id = str(info.get('id', '') or '').strip()
                if child_id and bool(info.get('is_dir', False)):
                    q.append((PurePosixPath(child_path), child_id))

        return results
    
    def _offline_space(self) -> Dict:
        url = 'https://115.com/?ct=offline&ac=space'
        resp = self.request(Method.GET, url)
        if resp:
            return resp.json()
        return {}

    def add_lixian_task(self, links: List[str], pan_path: str) -> List[Dict]:
        """
        返回任务hash
        """
        if self.create_folder(pan_path) == '-1':
            return []
        
        wp_path_id = self.path_to_id(pan_path, expect=PathExpect.DIR)
        if not wp_path_id:
            return []

        return self.add_lixian_task_id(links, wp_path_id)
    
    def add_lixian_task_id(self, links: List[str], wp_path_id: str) -> List[Dict]:
        """
        返回任务hash（直接传入目录 id）
        """
        results = []
        wp_path_id = str(wp_path_id or '').strip()
        if not wp_path_id:
            return results
        off_space_data = self._offline_space()
        
        url = 'https://115.com/web/lixian/?ct=lixian&ac=add_task_urls'
        form_data = {
            'sign': off_space_data.get('sign', ''),
            'time': str(off_space_data.get('time', '')),
            'uid': self.uid,
            'savepath': '',
            'wp_path_id': wp_path_id,
        }
        for i, link in enumerate(links):
            form_data[f'url[{i}]'] = link
        
        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data.get('errno') == 0:
                tasks = data.get('result')
                for task in tasks:
                    results.append({
                        'info_hash': task.get('info_hash'),
                        'url': task.get('url'),
                        'name': task.get('name'),
                    })
        return results

    def get_lixian_tasks(self, type: int=12) -> Dict:
        """
        12: 正在下载
        11: 完成记录
        9: 下载失败
        
        结果说明:
        wp_path_id: 存储在的文件夹id
        file_id: 文件id(目录id)
        info_hash: 任务hash
        """
        off_space_data = self._offline_space()
        url = 'https://115.com/web/lixian/?ct=lixian&ac=task_lists'
        form_data = {
            'page': '1',
            'stat': str(type),
            'uid': self.uid,
            'sign': off_space_data.get('sign', ''),
            'time': str(off_space_data.get('time', '')),
        }
        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data['state'] == True:
                return data
        return {}
    
    def move(self, paths: List[str], dest_path: str, expect: PathExpect = PathExpect.ANY) -> bool:
        if not paths or not dest_path:
            return True
        
        if not isinstance(expect, PathExpect):
            raise TypeError(f"expect must be PathExpect, got {type(expect).__name__}")
        src_ids = self.paths_to_ids(paths, expect=expect)
        dest_id = self.create_folder(dest_path)
        if dest_id == '-1':
            return False
        return self.move_ids(src_ids, dest_id)

    def move_ids(self, src_ids: List[str], dest_id: str) -> bool:
        """
        直接传入 id 列表进行移动
        """
        if not src_ids:
            return True
        dest_id = str(dest_id or '').strip()
        if not dest_id or dest_id == '-1':
            return False
        url = 'https://webapi.115.com/files/move'
        
        form_data = {
            'pid': dest_id,
            'format': 'json',
        }
        j = 0
        for src_id in src_ids:
            src_id = str(src_id or '').strip()
            if not src_id or src_id == '-1':
                continue
            form_data[f'fid[{j}]'] = src_id
            j += 1
        if j == 0:
            return False
        
        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data['state'] == True:
                return True
        return False
    
    def delete(self, paths: List[str], expect: PathExpect = PathExpect.ANY) -> bool:
        if not paths:
            return True
        
        if not isinstance(expect, PathExpect):
            raise TypeError(f"expect must be PathExpect, got {type(expect).__name__}")
        ids_to_delete = self.paths_to_ids(paths, expect=expect)
        return self.delete_ids(ids_to_delete)

    def delete_ids(self, ids_to_delete: List[str]) -> bool:
        """
        直接传入 id 列表进行删除
        """
        if not ids_to_delete:
            return True
        url = 'https://webapi.115.com/rb/delete'
        
        form_data = {
            'ignore_warn': '0',
        }
        j = 0
        for src_id in ids_to_delete:
            src_id = str(src_id or '').strip()
            if not src_id or src_id == '-1':
                continue
            form_data[f'fid[{j}]'] = src_id
            j += 1
        if j == 0:
            return True
        
        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data['state'] == True:
                return True
        return False    
    
    def rename(self, paths2newname: Dict[str, str], expect: PathExpect = PathExpect.ANY) -> bool:
        paths = list(paths2newname.keys())
        if not isinstance(expect, PathExpect):
            raise TypeError(f"expect must be PathExpect, got {type(expect).__name__}")
        ids_to_rename = self.paths_to_ids(paths, expect=expect)
        
        id2newname: Dict[str, str] = {}
        for i in range(len(ids_to_rename)):
            rid = str(ids_to_rename[i] or '').strip()
            if not rid or rid == '-1':
                continue
            id2newname[rid] = paths2newname[paths[i]]
        return self.rename_ids(id2newname)

    def rename_ids(self, id2newname: Dict[str, str]) -> bool:
        """
        直接传入 id->新名字 进行重命名（不走 path_to_id/paths_to_ids）
        """
        if not id2newname:
            return False

        url = 'https://webapi.115.com/files/batch_rename'
        form_data = {
            'format': 'json'
        }
        j = 0
        for rid, newname in id2newname.items():
            rid = str(rid or '').strip()
            if not rid or rid == '-1':
                continue
            form_data[f'files_new_name[{rid}]'] = newname
            j += 1
        if j == 0:
            return False

        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data['state'] == True:
                return True
        return False
    
    def create_folder(self, folder_path: str) -> str:
        """
        返回创建的文件夹的id
        """
        return self.create_folder_to_id('0', folder_path)

    def create_folder_in(self, parent_id: str, folder_name: str) -> str:
        """
        在指定 parent_id 下创建一个子文件夹，返回新文件夹 id，失败返回 -1
        """
        parent_id = str(parent_id or '').strip()
        folder_name = str(folder_name or '').strip()
        if not parent_id or not folder_name:
            return '-1'

        url = 'https://webapi.115.com/files/add'
        form_data = {
            'pid': parent_id,
            'cname': folder_name
        }
        resp = self.request(Method.POST, url, data=form_data)
        if resp:
            data = resp.json()
            if data.get('state') == True:
                return str(data.get('cid', '') or '').strip() or '-1'
        return '-1'

    def create_folder_to_id(self, parent_id: str, folder_path: str, limit: str = '1000') -> str:
        """
        在指定 parent_id 下按 folder_path 创建多级子文件夹，返回最终文件夹 id，失败返回 -1。
        """
        parent_id = str(parent_id or '').strip()
        if not parent_id:
            return '-1'

        p = PurePosixPath(folder_path)
        parts = [seg for seg in p.parts if seg not in ('/', '')]
        if not parts:
            return parent_id

        cur_id = parent_id
        for seg in parts:
            children = self.list_files(cur_id, limit)
            existing_dir = next((c for c in children if c.get('name') == seg and bool(c.get('is_dir', False))), None)
            if existing_dir:
                next_id = str(existing_dir.get('id', '') or '').strip()
                if not next_id:
                    return '-1'
                cur_id = next_id
                continue

            new_id = self.create_folder_in(cur_id, seg)
            if new_id == '-1':
                return '-1'
            cur_id = new_id

        return cur_id

    def upload_file_id(self, local_path: str, pan_path_id: str) -> str:
        """
        不支持大文件分片上传
        """
        filename = PurePosixPath(local_path).name
        
         # 获取上传信息
        url = 'https://uplb.115.com/3.0/sampleinitupload.php'
        form_data = {
            'userid': self.uid,
            'filename': filename,
            'filesize': str(os.path.getsize(local_path)),
            'target': f'U_1_{pan_path_id}',
        }
        
        resp = self.request(Method.POST, url, data=form_data)
        if not resp:
            return '-1'
        # {"object":"69836aa2e4b80bc9fc4a9ecff879174b488390ad","accessid":"LTAI5tMvF188zm8v4bVG2BQC","host":"https:\/\/fhnfile.oss-cn-shenzhen.aliyuncs.com","policy":"eyJleHBpcmF0aW9uIjoiMjAyNi0wMi0wNFQyMzo1MDoyNFoiLCJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMCw1MzY4NzA5MTIwXV19","signature":"qw+QflqvMxMH4wDIJcwnCt4KrAU=","expire":1770220224,"callback":"eyJjYWxsYmFja1VybCI6Imh0dHA6XC9cL3VwbGIuMTE1LmNvbVwvMy4wXC9zYW1wbGVjb21wbGV0ZXVwbG9hZC5waHAiLCJjYWxsYmFja0JvZHkiOiJzaGExPSR7c2hhMX0mZmlsZW5hbWU9JHtvYmplY3R9JmZpbGVzaXplPSR7c2l6ZX0mbWltZVR5cGU9JHttaW1lVHlwZX0maGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0md2lkdGg9JHtpbWFnZUluZm8ud2lkdGh9JnVzZXJpZD0xMDMyMDE2NTQmdGFyZ2V0PVVfMV8zMzU3MDczMjM0NjcwMzg2ODQwJnVzZXJmbj1NRGhETUVFeE1UVTFSVVl6UlRKR1FrTTVOVVE1UWtJNE5URXpOamc1UlVSQ1F6QXpNRGxFTXk1emNuUT0mYnVja2V0PWZobmZpbGUmdXNlcmlwPTExMC43My44Ny41NyZ1c2VycG9ydD0zODAwMCZzb3VyY2U9NCZjYl90b2tlbj0zZTQyZTY3NDY5ODU4Yjg5YzJmYjBhM2E4NGNiNmU5ZCIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvblwveC13d3ctZm9ybS11cmxlbmNvZGVkIn0="}
        upload_info = resp.json()
        upload_url = upload_info.get('host', '')
        if not upload_url:
            return '-1'
        files = {
            'file': open(local_path, 'rb')
        }
        form_data = {
            'name': filename,
            'policy': upload_info.get('policy', ''),
            'OSSAccessKeyId': upload_info.get('accessid', ''),
            'success_action_status': '200',
            'callback': upload_info.get('callback', ''),
            'signature': upload_info.get('signature', ''),
            'key': upload_info.get('object', ''),
            'target': f'U_1_{pan_path_id}',
        }
        resp = self.request(Method.POST, upload_url, data=form_data, files=files)

        if not resp:
            return '-1'
        data = resp.json()
        if data.get('state') == True:
            return data.get('data', {}).get('file_id', '-1')
        return '-1'

    def upload_files(self, local_paths: List[str], pan_path: str) -> List[str]:
        """
        不支持大文件分片上传
        """
        wp_path_id = self.create_folder(pan_path)
        if wp_path_id == '-1':
            return []
        results = []
        for local_path in local_paths:
            file_id = self.upload_file_id(local_path, wp_path_id)
            results.append(file_id)
            
        return results   
        