"""
MDV - Markdown Viewer Server
ファイルツリー表示 + マークダウンプレビュー + ホットリロード
"""

from __future__ import annotations

import asyncio
import json
import mimetypes
import re
import socket
import webbrowser
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Set

import markdown
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query, HTTPException
from fastapi.responses import FileResponse as FastAPIFileResponse
from fastapi.staticfiles import StaticFiles
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

from .file_types import (
    FILE_TYPES,
    SUPPORTED_EXTENSIONS,
    SKIP_DIRECTORIES,
    SKIP_FILES,
    get_file_type,
    FileTypeInfo,
)
from .models import SaveFileRequest


# === Application State ===

@dataclass
class AppState:
    """アプリケーション状態を管理"""
    root_path: Path = field(default_factory=Path.cwd)
    connected_clients: Set[WebSocket] = field(default_factory=set)
    current_watching_file: Optional[str] = None
    observer: Optional[Observer] = None
    event_handler: Optional["FileChangeHandler"] = None

    def set_root_path(self, path: str | Path) -> None:
        self.root_path = Path(path).resolve()

    def add_client(self, client: WebSocket) -> None:
        self.connected_clients.add(client)

    def remove_client(self, client: WebSocket) -> None:
        self.connected_clients.discard(client)

    def set_watching_file(self, path: str) -> None:
        self.current_watching_file = path


# シングルトンインスタンス
state = AppState()


# === File Change Handler ===

class FileChangeHandler(FileSystemEventHandler):
    """ファイル変更を検知してWebSocketでクライアントに通知"""

    def __init__(self) -> None:
        super().__init__()
        self._loop: Optional[asyncio.AbstractEventLoop] = None

    def set_loop(self, loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop

    def _handle_file_change(self, file_path: str) -> None:
        """ファイル変更を処理"""
        if not state.current_watching_file:
            return
        if not self._loop:
            return

        # パスを正規化して比較（watchdogとresolve()の形式が異なる場合がある）
        try:
            normalized_path = str(Path(file_path).resolve())
        except Exception:
            normalized_path = file_path

        if normalized_path != state.current_watching_file:
            return

        asyncio.run_coroutine_threadsafe(
            broadcast_file_update(normalized_path),
            self._loop
        )

    def _handle_tree_change(self) -> None:
        """ファイルツリーの変更を通知"""
        if not self._loop:
            return
        asyncio.run_coroutine_threadsafe(
            broadcast_tree_update(),
            self._loop
        )

    def on_modified(self, event) -> None:
        if event.is_directory:
            return
        self._handle_file_change(event.src_path)

    def on_moved(self, event) -> None:
        """ファイルのリネーム/移動を検知（アトミック書き込み対応）"""
        if event.is_directory:
            return
        self._handle_file_change(event.dest_path)
        self._handle_tree_change()

    def on_created(self, event) -> None:
        """ファイル/ディレクトリ作成を検知"""
        self._handle_tree_change()

    def on_deleted(self, event) -> None:
        """ファイル/ディレクトリ削除を検知"""
        self._handle_tree_change()


# === Rendering Functions ===

def escape_html(text: str) -> str:
    """HTMLエスケープ"""
    return (
        text
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )


# リストアイテムのパターン
_LIST_ITEM_PATTERN = re.compile(r'^[ \t]*(?:\d+\.|[-*+])[ \t]+')

# YAMLフロントマターのパターン（ファイル先頭の---で囲まれた部分）
_FRONTMATTER_PATTERN = re.compile(r'^---\s*\n(.*?)\n---\s*(\n|$)', re.DOTALL)


def _preprocess_markdown(content: str) -> str:
    """マークダウンの前処理（YAMLフロントマター変換、リスト前空行挿入）"""
    # YAMLフロントマターをコードブロックに変換
    # ---で囲まれた部分はMarkdownで<hr>として解釈されるため、
    # 改行が失われる問題を回避
    frontmatter_match = _FRONTMATTER_PATTERN.match(content)
    if frontmatter_match:
        frontmatter_content = frontmatter_match.group(1)
        rest_of_content = content[frontmatter_match.end():]
        content = f"```yaml\n{frontmatter_content}\n```\n{rest_of_content}"

    # リストの開始前にのみ空行を自動挿入（python-markdown互換性のため）
    lines = content.split('\n')
    result = []

    for i, line in enumerate(lines):
        if i > 0 and _LIST_ITEM_PATTERN.match(line):
            prev_line = lines[i - 1]
            # 前の行が空でなく、かつリストアイテムでもない場合に空行を挿入
            if prev_line.strip() and not _LIST_ITEM_PATTERN.match(prev_line):
                result.append('')
        result.append(line)

    return '\n'.join(result)


def render_markdown(content: str) -> str:
    """マークダウンをHTMLに変換"""
    content = _preprocess_markdown(content)
    md = markdown.Markdown(
        extensions=["fenced_code", "codehilite", "tables", "toc", "sane_lists"],
        tab_length=2,  # 2スペースでネストしたリストを認識
    )
    return md.convert(content)


def render_code(content: str, lang: Optional[str] = None) -> str:
    """コードをシンタックスハイライト用HTMLに変換"""
    escaped = escape_html(content)
    lang_class = f"language-{lang}" if lang else ""
    return f'<pre><code class="{lang_class}">{escaped}</code></pre>'


def render_text(content: str) -> str:
    """プレーンテキストをHTMLに変換"""
    escaped = escape_html(content)
    return f'<pre class="plain-text">{escaped}</pre>'


def render_file_content(content: str, file_info: FileTypeInfo) -> str:
    """ファイルタイプに応じてコンテンツをレンダリング"""
    if file_info.type == "markdown":
        return render_markdown(content)
    elif file_info.type == "code":
        return render_code(content, file_info.lang)
    else:
        return render_text(content)


# === WebSocket Broadcasting ===

async def broadcast_tree_update() -> None:
    """全クライアントにファイルツリー更新を通知"""
    if not state.connected_clients:
        return

    try:
        tree = get_file_tree(state.root_path)
        message = json.dumps({
            "type": "tree_update",
            "tree": tree,
        })

        disconnected = []
        for client in state.connected_clients:
            try:
                await client.send_text(message)
            except Exception:
                disconnected.append(client)

        for client in disconnected:
            state.remove_client(client)

    except Exception as e:
        print(f"Error broadcasting tree update: {e}")


async def broadcast_file_update(file_path: str) -> None:
    """全クライアントにファイル更新を通知"""
    if not state.connected_clients:
        return

    try:
        path = Path(file_path)
        file_info = get_file_type(path.suffix)

        if not file_info:
            return

        # メッセージ作成
        if file_info.type == "image":
            message = {
                "type": "file_update",
                "path": file_path,
                "fileType": "image",
                "reload": True,
            }
        else:
            content = path.read_text(encoding="utf-8")
            html_content = render_file_content(content, file_info)
            message = {
                "type": "file_update",
                "path": file_path,
                "content": html_content,
                "fileType": file_info.type,
            }

        message_json = json.dumps(message)

        # 全クライアントに送信
        disconnected = []
        for client in state.connected_clients:
            try:
                await client.send_text(message_json)
            except Exception:
                disconnected.append(client)

        # 切断されたクライアントを削除
        for client in disconnected:
            state.remove_client(client)

    except Exception as e:
        print(f"Error broadcasting update: {e}")


# === File Tree ===

def get_file_tree(root: Path) -> list:
    """ディレクトリツリーを取得（サポートするファイルタイプのみ）"""
    items = []

    try:
        entries = sorted(
            root.iterdir(),
            key=lambda x: (not x.is_dir(), x.name.lower())
        )
    except PermissionError:
        return items

    for entry in entries:
        # 特定ディレクトリをスキップ
        if entry.name in SKIP_DIRECTORIES:
            continue
        # ゴミファイルをスキップ
        if entry.name in SKIP_FILES:
            continue

        rel_path = str(entry.relative_to(state.root_path))

        if entry.is_dir():
            children = get_file_tree(entry)
            # サポートするファイルを含むディレクトリのみ表示
            if children:
                items.append({
                    "name": entry.name,
                    "path": rel_path,
                    "type": "directory",
                    "children": children,
                })
        elif entry.suffix.lower() in SUPPORTED_EXTENSIONS:
            file_info = FILE_TYPES[entry.suffix.lower()]
            items.append({
                "name": entry.name,
                "path": rel_path,
                "type": "file",
                "fileType": file_info.type,
                "icon": file_info.icon,
                "lang": file_info.lang,
            })

    return items


# === Security ===

def validate_path(requested_path: str) -> Path:
    """
    パスを検証してセキュアなPathオブジェクトを返す
    不正なパスの場合はHTTPExceptionを発生
    """
    file_path = state.root_path / requested_path

    if not file_path.exists():
        raise HTTPException(status_code=404, detail="File not found")

    # ROOT_PATH外へのアクセスを防ぐ
    try:
        file_path.resolve().relative_to(state.root_path.resolve())
    except ValueError:
        raise HTTPException(status_code=403, detail="Access denied")

    return file_path


# === FastAPI Application ===

app = FastAPI(title="MDV - Markdown Viewer")


@app.on_event("startup")
async def startup_event() -> None:
    """サーバー起動時にイベントループを設定"""
    if state.event_handler:
        loop = asyncio.get_running_loop()
        state.event_handler.set_loop(loop)
        print("✅ File watcher connected to event loop")


@app.get("/")
async def index() -> FastAPIFileResponse:
    """メインページ"""
    static_dir = Path(__file__).parent / "static"
    return FastAPIFileResponse(static_dir / "index.html")


@app.get("/api/tree")
async def get_tree() -> list:
    """ファイルツリーを取得"""
    return get_file_tree(state.root_path)


@app.get("/api/file")
async def get_file(path: str = Query(...)) -> dict:
    """ファイルを取得してレンダリング"""
    file_path = validate_path(path)

    if not file_path.is_file():
        raise HTTPException(status_code=400, detail="Not a file")

    file_info = get_file_type(file_path.suffix)
    if not file_info:
        raise HTTPException(status_code=400, detail="Unsupported file type")

    # 監視対象を更新
    state.set_watching_file(str(file_path.resolve()))

    # 画像の場合
    if file_info.type == "image":
        return {
            "path": path,
            "name": file_path.name,
            "fileType": file_info.type,
            "imageUrl": f"/api/image?path={path}",
        }

    # PDFの場合
    if file_info.type == "pdf":
        return {
            "path": path,
            "name": file_path.name,
            "fileType": file_info.type,
            "pdfUrl": f"/api/pdf?path={path}",
        }

    # テキスト系ファイルを読み込み
    try:
        content = file_path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        raise HTTPException(status_code=400, detail="Cannot read binary file as text")

    html_content = render_file_content(content, file_info)

    return {
        "path": path,
        "name": file_path.name,
        "content": html_content,
        "raw": content,
        "fileType": file_info.type,
        "lang": file_info.lang,
    }


@app.get("/api/image")
async def get_image(path: str = Query(...)) -> FastAPIFileResponse:
    """画像ファイルを返す"""
    file_path = validate_path(path)

    mime_type, _ = mimetypes.guess_type(str(file_path))
    if not mime_type or not mime_type.startswith("image/"):
        raise HTTPException(status_code=400, detail="Not an image file")

    return FastAPIFileResponse(file_path, media_type=mime_type)


@app.get("/api/pdf")
async def get_pdf(path: str = Query(...)) -> FastAPIFileResponse:
    """PDFファイルを返す"""
    file_path = validate_path(path)

    if not file_path.suffix.lower() == ".pdf":
        raise HTTPException(status_code=400, detail="Not a PDF file")

    return FastAPIFileResponse(file_path, media_type="application/pdf")


@app.post("/api/file")
async def save_file(request: SaveFileRequest) -> dict:
    """ファイルを保存"""
    file_path = validate_path(request.path)

    if not file_path.is_file():
        raise HTTPException(status_code=400, detail="Not a file")

    file_info = get_file_type(file_path.suffix)
    if not file_info or file_info.type == "image":
        raise HTTPException(status_code=400, detail="Cannot edit this file type")

    try:
        file_path.write_text(request.content, encoding="utf-8")
        return {"success": True, "path": request.path}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to save: {str(e)}")


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
    """WebSocket接続を管理"""
    await websocket.accept()
    state.add_client(websocket)

    try:
        while True:
            data = await websocket.receive_text()
            message = json.loads(data)

            if message.get("type") == "watch":
                file_path = state.root_path / message.get("path", "")
                if file_path.exists():
                    state.set_watching_file(str(file_path.resolve()))

    except WebSocketDisconnect:
        state.remove_client(websocket)
    except Exception as e:
        print(f"WebSocket error: {e}")
        state.remove_client(websocket)


# 静的ファイルをマウント
static_dir = Path(__file__).parent / "static"
if static_dir.exists():
    app.mount("/static", StaticFiles(directory=static_dir), name="static")


# === Server Startup ===

def find_available_port(start_port: int, max_attempts: int = 100) -> int:
    """利用可能なポートを探す"""
    for offset in range(max_attempts):
        port = start_port + offset
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.bind(("localhost", port))
                return port
        except OSError:
            continue

    raise RuntimeError(
        f"No available port found in range {start_port}-{start_port + max_attempts}"
    )


def start_server(
    root_path: str = ".",
    port: int = 8642,
    open_browser: bool = True,
    initial_file: Optional[str] = None,
) -> None:
    """サーバーを起動"""
    state.set_root_path(root_path)

    if not state.root_path.exists():
        print(f"Error: Path does not exist: {state.root_path}")
        return

    # 利用可能なポートを探す
    try:
        actual_port = find_available_port(port)
        if actual_port != port:
            print(f"⚠️  Port {port} is in use, using {actual_port} instead")
    except RuntimeError as e:
        print(f"Error: {e}")
        return

    print(f"📁 Serving: {state.root_path}")
    print(f"🌐 URL: http://localhost:{actual_port}")

    # ファイル監視を開始
    state.event_handler = FileChangeHandler()
    state.observer = Observer()
    state.observer.schedule(
        state.event_handler,
        str(state.root_path),
        recursive=True
    )
    state.observer.start()

    # ブラウザを開く
    if open_browser:
        url = f"http://localhost:{actual_port}"
        if initial_file:
            from urllib.parse import quote
            url += f"?file={quote(initial_file)}"
        webbrowser.open(url)

    # サーバー起動
    try:
        config = uvicorn.Config(
            app,
            host="0.0.0.0",
            port=actual_port,
            log_level="warning"
        )
        server = uvicorn.Server(config)
        asyncio.run(server.serve())
    except KeyboardInterrupt:
        pass
    finally:
        if state.observer:
            state.observer.stop()
            state.observer.join()


if __name__ == "__main__":
    start_server()
