import io
import os
import tempfile
import urllib.request
import urllib.parse
import xml.etree.ElementTree as ET

from flask import (
    Flask,
    jsonify,
    render_template,
    request,
    send_file,
    redirect,
    url_for,
    session,
    flash,
)
import fitz  # PyMuPDF
from PIL import Image
import psycopg2

from werkzeug.security import (
    generate_password_hash,
    check_password_hash,
)

from functools import wraps

app = Flask(__name__)

app.secret_key = "change-this-secret-key"

GITHUB_PDF_BASE_URL = "https://github.io"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BOOKSHELF_DIR = os.path.join(BASE_DIR, "bookshelf")

def login_required(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        if "user_id" not in session:
            return redirect(url_for("login"))

        return func(*args, **kwargs)

    return wrapper


def get_connection():
    return psycopg2.connect(os.environ["DATABASE_URL"])


def init_db():
    """データベースの初期化"""
    conn = get_connection()
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS bookmarks (
            id BIGSERIAL PRIMARY KEY,
            book_name TEXT NOT NULL,
            page_number INTEGER NOT NULL DEFAULT 1,
            created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
            updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
        )
    """)
    conn.commit()
    cur.close()
    conn.close()

@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        username = request.form["username"].strip()
        password = request.form["password"]

        if username == "" or password == "":
            flash("ユーザー名とパスワードを入力してください。")
            return redirect(url_for("register"))

        password_hash = generate_password_hash(password)

        conn = psycopg2.connect(os.environ["DATABASE_URL"])
        cur = conn.cursor()

        try:
            cur.execute(
                """
                INSERT INTO users (username, password_hash)
                VALUES (%s, %s)
                """,
                (username, password_hash),
            )
            conn.commit()

        except psycopg2.Error:
            conn.rollback()
            flash("そのユーザー名は既に使用されています。")
            cur.close()
            conn.close()
            return redirect(url_for("register"))

        cur.close()
        conn.close()

        flash("登録が完了しました。ログインしてください。")
        return redirect(url_for("login"))

    return render_template("register.html")

@app.route("/login", methods=["GET", "POST"])
def login():

    if request.method == "POST":

        username = request.form["username"].strip()
        password = request.form["password"]

        conn = psycopg2.connect(os.environ["DATABASE_URL"])
        cur = conn.cursor()

        cur.execute(
            """
            SELECT id, password_hash
            FROM users
            WHERE username=%s
            AND deleted_at IS NULL
            """,
            (username,),
        )

        user = cur.fetchone()

        cur.close()
        conn.close()

        if user and check_password_hash(user[1], password):

            session["user_id"] = user[0]
            session["username"] = username

            return redirect(url_for("index"))

        flash("ユーザー名またはパスワードが違います。")

    return render_template("login.html")

@app.route("/logout")
def logout():

    session.clear()

    return redirect(url_for("login"))    


@app.route("/")
@login_required
def index():
    """PDFの文書プロパティの『タイトル』を引用して本棚を作ります"""
    books = []
    if os.path.exists(BOOKSHELF_DIR):
        for file_name in sorted(os.listdir(BOOKSHELF_DIR)):
            if file_name.lower().endswith(".pdf"):
                pdf_path = os.path.join(BOOKSHELF_DIR, file_name)
                try:
                    doc = fitz.open(pdf_path)
                    metadata = doc.metadata
                    doc.close()
                    
                    book_title = metadata.get("title", "").strip() if metadata else ""
                    if not book_title:
                        book_title = os.path.splitext(file_name)[0]
                        
                    books.append(book_title)
                except Exception as e:
                    print(f"PDFのプロパティ読み込みに失敗 ({file_name}): {e}")
                    
    return render_template("index.html", books=books)


@app.route("/api/book/<path:book_name>")
def get_book(book_name):
    """プロパティのキーワード（keywords）欄からXMLを安全に抽出し同期するAPI"""
    playlist = {}
    total_pages = 0
    metadata = {}
    pdf_file = None

    try:
        if os.path.exists(BOOKSHELF_DIR):
            for file_name in os.listdir(BOOKSHELF_DIR):
                if file_name.lower().endswith(".pdf"):
                    check_path = os.path.join(BOOKSHELF_DIR, file_name)
                    try:
                        tmp_doc = fitz.open(check_path)
                        tmp_meta = tmp_doc.metadata
                        tmp_doc.close()
                        
                        title_in_pdf = tmp_meta.get("title", "").strip() if tmp_meta else ""
                        if not title_in_pdf:
                            title_in_pdf = os.path.splitext(file_name)[0]
                            
                        if title_in_pdf == book_name:
                            pdf_file = check_path
                            break
                    except Exception:
                        continue

        if not pdf_file:
            return jsonify({"error": f"タイトル '{book_name}' に該当するPDFファイルが見つかりませんでした。"}), 404

        doc = fitz.open(pdf_file)
        total_pages = len(doc)
        metadata = doc.metadata
        doc.close()

    except Exception as e:
        print(f"[PDF ERROR] PDFの読み込みに失敗しました: {e}")
        return jsonify({"error": f"PDFの読み込みに失敗しました: {e}"}), 500

    # 元アプリの仕様に基づき「keywords」欄からXMLのURLを最優先で取得
    xml_url = ""
    if metadata:
        xml_url = metadata.get("keywords", "").strip()
        if not xml_url:
            xml_url = metadata.get("subject", "").strip()

    if xml_url and (xml_url.startswith("http://") or xml_url.startswith("https://")):
        try:
            print(f"[XML LOAD] プロパティのキーワードからXMLを読み込みます: {xml_url}")
            x_scheme, x_netloc, x_path, x_query, x_fragment = urllib.parse.urlsplit(xml_url)
            x_path = urllib.parse.quote(x_path)
            encoded_xml_url = urllib.parse.urlunsplit((x_scheme, x_netloc, x_path, x_query, x_fragment))

            req = urllib.request.Request(
                encoded_xml_url, 
                headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
            )
            with urllib.request.urlopen(req, timeout=5) as response:
                xml_data = response.read()
                
            root = ET.fromstring(xml_data.decode("utf-8"))
            for track in root.findall("Track"):
                # 元アプリの仕様通り、内部ページインデックス（Page - 1）で同期を構築
                page_text = track.find("Page").text
                if page_text:
                    page = int(page_text.strip()) - 1
                else:
                    continue
                    
                music_path_node = track.find("MusicPath")
                if music_path_node is not None and music_path_node.text:
                    music_path = music_path_node.text.strip()
                else:
                    continue
                
                if not (music_path.startswith("http://") or music_path.startswith("https://")):
                    music_file = os.path.basename(music_path)
                    playlist[page] = f"/music/{urllib.parse.quote(music_file)}"
                else:
                    playlist[page] = music_path
                    
            print(f"[XML SUCCESS] プレイリストを同期しました: {len(playlist)}個の割り当て完了")
        except Exception as e:
            print(f"[XML ERROR] プロパティXMLの取得・解析に失敗しました: {e}")

    return jsonify({
        "book_name": book_name,
        "total_pages": total_pages,
        "playlist": playlist
    })


@app.route("/page/<path:book_name>/<int:page_number>")
def render_page(book_name, page_number):
    """本のタイトル名またはURLから指定されたページを画像化して配信"""
    is_url = book_name.startswith("http://") or book_name.startswith("https://")
    pdf_file = None
    
    try:
        if is_url:
            scheme, netloc, path, query, fragment = urllib.parse.urlsplit(book_name)
            path = urllib.parse.quote(path)
            encoded_pdf_url = urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
            
            req = urllib.request.Request(
                encoded_pdf_url, 
                headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
            )
            with urllib.request.urlopen(req, timeout=10) as response:
                pdf_data = response.read()
            doc = fitz.open(stream=pdf_data, filetype="pdf")
        else:
            if os.path.exists(BOOKSHELF_DIR):
                for file_name in os.listdir(BOOKSHELF_DIR):
                    if file_name.lower().endswith(".pdf"):
                        check_path = os.path.join(BOOKSHELF_DIR, file_name)
                        try:
                            tmp_doc = fitz.open(check_path)
                            tmp_meta = tmp_doc.metadata
                            tmp_doc.close()
                            
                            title_in_pdf = tmp_meta.get("title", "").strip() if tmp_meta else ""
                            if not title_in_pdf:
                                title_in_pdf = os.path.splitext(file_name)[0]
                                
                            if title_in_pdf == book_name:
                                pdf_file = check_path
                                break
                        except Exception:
                            continue

            if not pdf_file:
                return f"タイトル '{book_name}' に該当するPDFファイルが見つかりません", 404
                
            doc = fitz.open(pdf_file)

    except Exception as e:
        print(f"[PAGE ERROR] PDFの読み込み失敗: {e}")
        return f"PDF load error: {e}", 500

    if page_number < 1 or page_number > len(doc):
        doc.close()
        return "Invalid page", 400

    page = doc.load_page(page_number - 1)

    zoom = float(request.args.get("zoom", "2.2"))
    matrix = fitz.Matrix(zoom, zoom)

    pix = page.get_pixmap(matrix=matrix)
    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    buffer.seek(0)

    doc.close()
    return send_file(buffer, mimetype="image/png")


@app.route("/bookmark/<path:book_name>", methods=["GET", "POST"])
@login_required
def bookmark(book_name):
    """データベースへの栞の保存と取得"""

    conn = get_connection()
    cur = conn.cursor()

    if request.method == "POST":

        page_number = int(request.json.get("page_number", 1))

        cur.execute(
            """
            SELECT id
            FROM bookmarks
            WHERE user_id = %s
            AND book_name = %s
            """,
            (session["user_id"], book_name)
        )

        row = cur.fetchone()

        if row:

            cur.execute(
                """
                UPDATE bookmarks
                SET page_number = %s,
                    updated_at = now()
                WHERE user_id = %s
                AND book_name = %s
                """,
                (
                    page_number,
                    session["user_id"],
                    book_name,
                )
            )

        else:

            cur.execute(
                """
                INSERT INTO bookmarks
                (
                    user_id,
                    book_name,
                    page_number
                )
                VALUES
                (
                    %s,
                    %s,
                    %s
                )
                """,
                (
                    session["user_id"],
                    book_name,
                    page_number,
                )
            )

        conn.commit()
        cur.close()
        conn.close()

        return jsonify({"status": "saved"})

    cur.execute(
        """
        SELECT page_number
        FROM bookmarks
        WHERE user_id = %s
        AND book_name = %s
        ORDER BY id DESC
        LIMIT 1
        """,
        (
            session["user_id"],
            book_name,
        )
    )

    row = cur.fetchone()

    cur.close()
    conn.close()

    return jsonify({
        "page_number": row[0] if row else 1
    })


@app.route("/music/<path:file_name>")
def music(file_name):
    """サーバー内のmusicフォルダから音楽ファイルをブラウザへ配信する"""
    music_dir = os.path.join(BASE_DIR, "music")
    decoded_name = urllib.parse.unquote(file_name)
    return send_file(os.path.join(music_dir, decoded_name))


if __name__ == "__main__":
    init_db()
    port = int(os.environ.get("PORT", 3000))
    app.run(host="0.0.0.0", port=port)
