import json
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
import uuid

from .auth import hash_password, hash_token, new_token, verify_password
from .models import SentenceBoundary, SentenceBoundaryDocument
from .store import normalize_video_hash


def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


class VideoRepository:
    def __init__(self, database_path: Path):
        self.database_path = Path(database_path)
        self.database_path.parent.mkdir(parents=True, exist_ok=True)
        self._initialize()

    def _connect(self) -> sqlite3.Connection:
        connection = sqlite3.connect(self.database_path, timeout=30)
        connection.row_factory = sqlite3.Row
        connection.execute("PRAGMA foreign_keys = ON")
        connection.execute("PRAGMA journal_mode = WAL")
        return connection

    def _initialize(self) -> None:
        with self._connect() as connection:
            connection.executescript(
                """
                CREATE TABLE IF NOT EXISTS videos (
                    video_hash TEXT PRIMARY KEY,
                    title TEXT NOT NULL,
                    filename TEXT NOT NULL,
                    stored_filename TEXT NOT NULL,
                    content_type TEXT NOT NULL,
                    size_bytes INTEGER NOT NULL,
                    duration_ms INTEGER,
                    language TEXT,
                    status TEXT NOT NULL,
                    error_message TEXT,
                    algorithm_version TEXT,
                    transcription TEXT,
                    remote_url TEXT,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS sentences (
                    video_hash TEXT NOT NULL,
                    sentence_index INTEGER NOT NULL,
                    start_ms INTEGER NOT NULL,
                    end_ms INTEGER NOT NULL,
                    text TEXT,
                    language TEXT,
                    reference_speech_duration_ms INTEGER,
                    PRIMARY KEY (video_hash, sentence_index),
                    FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS attempts (
                    attempt_id TEXT PRIMARY KEY,
                    video_hash TEXT NOT NULL,
                    sentence_index INTEGER NOT NULL,
                    result_json TEXT NOT NULL,
                    audio_filename TEXT,
                    created_at TEXT NOT NULL,
                    FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS users (
                    id TEXT PRIMARY KEY,
                    username TEXT NOT NULL UNIQUE COLLATE NOCASE,
                    nickname TEXT NOT NULL,
                    phone TEXT,
                    password_hash TEXT NOT NULL,
                    created_at TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS auth_tokens (
                    token_hash TEXT PRIMARY KEY,
                    user_id TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    expires_at TEXT NOT NULL,
                    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS enrollments (
                    user_id TEXT NOT NULL,
                    video_hash TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    PRIMARY KEY (user_id, video_hash),
                    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
                    FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS dub_shares (
                    share_id TEXT PRIMARY KEY,
                    video_hash TEXT NOT NULL,
                    title TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS dub_share_segments (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    share_id TEXT NOT NULL,
                    sentence_index INTEGER NOT NULL,
                    start_ms INTEGER NOT NULL,
                    end_ms INTEGER NOT NULL,
                    text TEXT,
                    audio_filename TEXT NOT NULL,
                    audio_size_bytes INTEGER NOT NULL,
                    overall_score REAL NOT NULL DEFAULT 0,
                    recognized_text TEXT NOT NULL DEFAULT '',
                    score_details TEXT NOT NULL DEFAULT '{}',
                    FOREIGN KEY (share_id) REFERENCES dub_shares(share_id) ON DELETE CASCADE,
                    UNIQUE (share_id, sentence_index)
                );
                """
            )

        with self._connect() as connection:
            columns = {
                row["name"]
                for row in connection.execute("PRAGMA table_info(dub_share_segments)")
            }
            if "overall_score" not in columns:
                connection.execute(
                    "ALTER TABLE dub_share_segments ADD COLUMN overall_score REAL NOT NULL DEFAULT 0"
                )
            if "recognized_text" not in columns:
                connection.execute(
                    "ALTER TABLE dub_share_segments ADD COLUMN recognized_text TEXT NOT NULL DEFAULT ''"
                )
            if "score_details" not in columns:
                connection.execute(
                    "ALTER TABLE dub_share_segments ADD COLUMN score_details TEXT NOT NULL DEFAULT '{}'"
                )

        with self._connect() as connection:
            attempt_columns = {
                row["name"]
                for row in connection.execute("PRAGMA table_info(attempts)")
            }
            if "user_id" not in attempt_columns:
                connection.execute("ALTER TABLE attempts ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")

        with self._connect() as connection:
            user_columns = {
                row["name"]
                for row in connection.execute("PRAGMA table_info(users)")
            }
            if "phone" not in user_columns:
                connection.execute("ALTER TABLE users ADD COLUMN phone TEXT")
            connection.execute("CREATE INDEX IF NOT EXISTS idx_attempts_user ON attempts(user_id)")

        with self._connect() as connection:
            dub_columns = {
                row["name"]
                for row in connection.execute("PRAGMA table_info(dub_shares)")
            }
            if "user_id" not in dub_columns:
                connection.execute("ALTER TABLE dub_shares ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")
            connection.execute("CREATE INDEX IF NOT EXISTS idx_dub_shares_user ON dub_shares(user_id)")

        with self._connect() as connection:
            video_columns = {
                row["name"]
                for row in connection.execute("PRAGMA table_info(videos)")
            }
            if "remote_url" not in video_columns:
                connection.execute("ALTER TABLE videos ADD COLUMN remote_url TEXT")

    def upsert_upload(
        self,
        *,
        video_hash: str,
        title: str,
        filename: str,
        stored_filename: str,
        content_type: str,
        size_bytes: int,
        language: Optional[str],
    ) -> None:
        normalized_hash = normalize_video_hash(video_hash)
        now = utc_now()
        with self._connect() as connection:
            existing = connection.execute(
                "SELECT created_at FROM videos WHERE video_hash = ?",
                (normalized_hash,),
            ).fetchone()
            created_at = existing["created_at"] if existing else now
            connection.execute(
                """
                INSERT INTO videos (
                    video_hash, title, filename, stored_filename, content_type,
                    size_bytes, duration_ms, language, status, error_message,
                    algorithm_version, transcription, remote_url, created_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, 'uploaded', NULL, NULL, NULL, NULL, ?, ?)
                ON CONFLICT(video_hash) DO UPDATE SET
                    title = excluded.title,
                    filename = excluded.filename,
                    stored_filename = excluded.stored_filename,
                    content_type = excluded.content_type,
                    size_bytes = excluded.size_bytes,
                    language = excluded.language,
                    status = 'uploaded',
                    error_message = NULL,
                    updated_at = excluded.updated_at
                """,
                (
                    normalized_hash,
                    title,
                    filename,
                    stored_filename,
                    content_type,
                    size_bytes,
                    language,
                    created_at,
                    now,
                ),
            )

    def mark_processing(self, video_hash: str) -> None:
        self._update_status(video_hash, "processing", None)

    def mark_failed(self, video_hash: str, message: str) -> None:
        self._update_status(video_hash, "failed", message[:4000])

    def _update_status(self, video_hash: str, status: str, error_message: Optional[str]) -> None:
        with self._connect() as connection:
            connection.execute(
                "UPDATE videos SET status = ?, error_message = ?, updated_at = ? WHERE video_hash = ?",
                (status, error_message, utc_now(), normalize_video_hash(video_hash)),
            )

    def save_processing_result(
        self,
        document: SentenceBoundaryDocument,
        transcription: Optional[str],
        ready: bool = True,
    ) -> None:
        with self._connect() as connection:
            connection.execute(
                "DELETE FROM sentences WHERE video_hash = ?",
                (document.video_hash,),
            )
            connection.executemany(
                """
                INSERT INTO sentences (
                    video_hash, sentence_index, start_ms, end_ms, text,
                    language, reference_speech_duration_ms
                ) VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                [
                    (
                        document.video_hash,
                        sentence.index,
                        sentence.start_ms,
                        sentence.end_ms,
                        sentence.text,
                        sentence.language,
                        sentence.reference_speech_duration_ms,
                    )
                    for sentence in document.sentences
                ],
            )
            connection.execute(
                """
                UPDATE videos SET duration_ms = ?, status = ?, error_message = NULL,
                    algorithm_version = ?, transcription = ?, updated_at = ?
                WHERE video_hash = ?
                """,
                (
                    document.duration_ms,
                    "ready" if ready else "processing",
                    document.algorithm_version,
                    transcription,
                    utc_now(),
                    document.video_hash,
                ),
            )

    def mark_oss_uploaded(self, video_hash: str, remote_url: str) -> None:
        with self._connect() as connection:
            connection.execute(
                """
                UPDATE videos
                SET remote_url = ?, status = 'ready', error_message = NULL, updated_at = ?
                WHERE video_hash = ?
                """,
                (remote_url, utc_now(), normalize_video_hash(video_hash)),
            )

    def list_videos(self) -> List[Dict[str, Any]]:
        with self._connect() as connection:
            rows = connection.execute(
                """
                SELECT v.*, COUNT(s.sentence_index) AS sentence_count
                FROM videos v
                LEFT JOIN sentences s ON s.video_hash = v.video_hash
                GROUP BY v.video_hash
                ORDER BY v.created_at DESC
                """
            ).fetchall()
        return [dict(row) for row in rows]

    def get_video(self, video_hash: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            row = connection.execute(
                """
                SELECT v.*, COUNT(s.sentence_index) AS sentence_count
                FROM videos v
                LEFT JOIN sentences s ON s.video_hash = v.video_hash
                WHERE v.video_hash = ?
                GROUP BY v.video_hash
                """,
                (normalize_video_hash(video_hash),),
            ).fetchone()
        return dict(row) if row else None

    def get_document(self, video_hash: str) -> Optional[SentenceBoundaryDocument]:
        video = self.get_video(video_hash)
        if video is None or video["duration_ms"] is None:
            return None
        with self._connect() as connection:
            rows = connection.execute(
                "SELECT * FROM sentences WHERE video_hash = ? ORDER BY sentence_index",
                (normalize_video_hash(video_hash),),
            ).fetchall()
        return SentenceBoundaryDocument(
            video_hash=video["video_hash"],
            duration_ms=video["duration_ms"],
            algorithm_version=video["algorithm_version"] or "unknown",
            sentences=[
                SentenceBoundary(
                    index=row["sentence_index"],
                    start_ms=row["start_ms"],
                    end_ms=row["end_ms"],
                    text=row["text"],
                    language=row["language"],
                    reference_speech_duration_ms=row["reference_speech_duration_ms"],
                )
                for row in rows
            ],
        )

    def update_sentence_text(
        self,
        video_hash: str,
        sentence_index: int,
        text: str,
        language: Optional[str],
    ) -> Optional[SentenceBoundary]:
        normalized_hash = normalize_video_hash(video_hash)
        with self._connect() as connection:
            cursor = connection.execute(
                """
                UPDATE sentences SET text = ?, language = COALESCE(?, language)
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (text.strip(), language, normalized_hash, sentence_index),
            )
            if cursor.rowcount == 0:
                return None
            connection.execute(
                "UPDATE videos SET updated_at = ? WHERE video_hash = ?",
                (utc_now(), normalized_hash),
            )
            row = connection.execute(
                "SELECT * FROM sentences WHERE video_hash = ? AND sentence_index = ?",
                (normalized_hash, sentence_index),
            ).fetchone()
        return SentenceBoundary(
            index=row["sentence_index"],
            start_ms=row["start_ms"],
            end_ms=row["end_ms"],
            text=row["text"],
            language=row["language"],
            reference_speech_duration_ms=row["reference_speech_duration_ms"],
        )

    def get_sentence(
        self,
        video_hash: str,
        sentence_index: int,
    ) -> Optional[SentenceBoundary]:
        normalized_hash = normalize_video_hash(video_hash)
        with self._connect() as connection:
            row = connection.execute(
                """
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (normalized_hash, sentence_index),
            ).fetchone()
        if row is None:
            return None
        return SentenceBoundary(
            index=row["sentence_index"],
            start_ms=row["start_ms"],
            end_ms=row["end_ms"],
            text=row["text"],
            language=row["language"],
            reference_speech_duration_ms=row["reference_speech_duration_ms"],
        )

    def replace_sentence(
        self,
        video_hash: str,
        sentence_index: int,
        replacements: List[SentenceBoundary],
    ) -> Optional[List[SentenceBoundary]]:
        """Replace one sentence row with one or more contiguous sentence rows."""
        normalized_hash = normalize_video_hash(video_hash)
        if not replacements:
            raise ValueError("At least one replacement sentence is required.")

        with self._connect() as connection:
            source = connection.execute(
                """
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (normalized_hash, sentence_index),
            ).fetchone()
            if source is None:
                return None

            rows = [
                SentenceBoundary(
                    index=index,
                    start_ms=replacement.start_ms,
                    end_ms=replacement.end_ms,
                    text=replacement.text.strip(),
                    language=replacement.language,
                    reference_speech_duration_ms=max(
                        1, replacement.reference_speech_duration_ms or 1
                    ),
                )
                for index, replacement in enumerate(replacements)
            ]
            previous_end = source["start_ms"]
            for replacement in rows:
                if (
                    replacement.start_ms < previous_end
                    or replacement.end_ms <= replacement.start_ms
                ):
                    raise ValueError("Replacement sentence times must be contiguous.")
                if replacement.end_ms > source["end_ms"]:
                    raise ValueError(
                        "Replacement sentences cannot exceed the source sentence."
                    )
                previous_end = replacement.end_ms
            if rows[-1].end_ms != source["end_ms"]:
                raise ValueError("Replacement sentences must cover the source sentence.")

            # Shift following rows down first. Descending indexes avoid violating
            # the (video_hash, sentence_index) primary key while making room.
            added_count = len(rows) - 1
            if added_count:
                following = [
                    row["sentence_index"]
                    for row in connection.execute(
                        """
                        SELECT sentence_index FROM sentences
                        WHERE video_hash = ? AND sentence_index > ?
                        ORDER BY sentence_index DESC
                        """,
                        (normalized_hash, sentence_index),
                    ).fetchall()
                ]
                for old_index in following:
                    connection.execute(
                        """
                        UPDATE sentences SET sentence_index = ?
                        WHERE video_hash = ? AND sentence_index = ?
                        """,
                        (old_index + added_count, normalized_hash, old_index),
                    )

            first = rows[0]
            connection.execute(
                """
                UPDATE sentences
                SET start_ms = ?, end_ms = ?, text = ?, language = ?,
                    reference_speech_duration_ms = ?
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (
                    first.start_ms,
                    first.end_ms,
                    first.text,
                    first.language,
                    first.reference_speech_duration_ms,
                    normalized_hash,
                    sentence_index,
                ),
            )
            for replacement in rows[1:]:
                connection.execute(
                    """
                    INSERT INTO sentences (
                        video_hash, sentence_index, start_ms, end_ms, text,
                        language, reference_speech_duration_ms
                    ) VALUES (?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        normalized_hash,
                        sentence_index + replacement.index,
                        replacement.start_ms,
                        replacement.end_ms,
                        replacement.text,
                        replacement.language,
                        replacement.reference_speech_duration_ms,
                    ),
                )
            connection.execute(
                "UPDATE videos SET updated_at = ? WHERE video_hash = ?",
                (utc_now(), normalized_hash),
            )
            result_rows = connection.execute(
                """
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index >= ? AND sentence_index < ?
                ORDER BY sentence_index
                """,
                (normalized_hash, sentence_index, sentence_index + len(rows)),
            ).fetchall()

        return [
            SentenceBoundary(
                index=row["sentence_index"],
                start_ms=row["start_ms"],
                end_ms=row["end_ms"],
                text=row["text"],
                language=row["language"],
                reference_speech_duration_ms=row["reference_speech_duration_ms"],
            )
            for row in result_rows
        ]

    def adjust_sentence_boundary(
        self,
        video_hash: str,
        sentence_index: int,
        delta_ms: int,
    ) -> Optional[List[SentenceBoundary]]:
        """Move one sentence end and link the next sentence's start to it."""
        normalized_hash = normalize_video_hash(video_hash)
        with self._connect() as connection:
            video = connection.execute(
                "SELECT duration_ms FROM videos WHERE video_hash = ?",
                (normalized_hash,),
            ).fetchone()
            if video is None or video["duration_ms"] is None:
                return None

            current = connection.execute(
                """
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (normalized_hash, sentence_index),
            ).fetchone()
            if current is None:
                return None

            following = connection.execute(
                """
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (normalized_hash, sentence_index + 1),
            ).fetchone()

            duration_ms = int(video["duration_ms"])
            minimum_end = current["start_ms"] + 1
            maximum_end = duration_ms
            if following is not None:
                maximum_end = min(maximum_end, following["end_ms"] - 1)
            new_end_ms = current["end_ms"] + delta_ms
            if new_end_ms < minimum_end or new_end_ms > maximum_end:
                raise ValueError(
                    "The adjusted boundary must remain inside both sentences."
                )

            current_duration_ms = new_end_ms - current["start_ms"]
            current_reference_ms = current["reference_speech_duration_ms"] or current_duration_ms
            current_reference_ms = min(
                current_duration_ms,
                max(1, current_reference_ms + delta_ms),
            )
            connection.execute(
                """
                UPDATE sentences
                SET end_ms = ?, reference_speech_duration_ms = ?
                WHERE video_hash = ? AND sentence_index = ?
                """,
                (
                    new_end_ms,
                    current_reference_ms,
                    normalized_hash,
                    sentence_index,
                ),
            )

            updated_indexes = [sentence_index]
            if following is not None:
                old_start_ms = following["start_ms"]
                next_duration_ms = following["end_ms"] - new_end_ms
                next_reference_ms = following["reference_speech_duration_ms"] or next_duration_ms
                next_reference_ms = min(
                    next_duration_ms,
                    max(1, next_reference_ms - (new_end_ms - old_start_ms)),
                )
                connection.execute(
                    """
                    UPDATE sentences
                    SET start_ms = ?, reference_speech_duration_ms = ?
                    WHERE video_hash = ? AND sentence_index = ?
                    """,
                    (
                        new_end_ms,
                        next_reference_ms,
                        normalized_hash,
                        sentence_index + 1,
                    ),
                )
                updated_indexes.append(sentence_index + 1)

            connection.execute(
                "UPDATE videos SET updated_at = ? WHERE video_hash = ?",
                (utc_now(), normalized_hash),
            )
            placeholders = ", ".join("?" for _ in updated_indexes)
            result_rows = connection.execute(
                f"""
                SELECT * FROM sentences
                WHERE video_hash = ? AND sentence_index IN ({placeholders})
                ORDER BY sentence_index
                """,
                (normalized_hash, *updated_indexes),
            ).fetchall()

        return [
            SentenceBoundary(
                index=row["sentence_index"],
                start_ms=row["start_ms"],
                end_ms=row["end_ms"],
                text=row["text"],
                language=row["language"],
                reference_speech_duration_ms=row["reference_speech_duration_ms"],
            )
            for row in result_rows
        ]

    def create_dub_share(
        self,
        *,
        video_hash: str,
        title: str,
        segments: List[Dict[str, Any]],
        user_id: str,
    ) -> Optional[Dict[str, Any]]:
        normalized_hash = normalize_video_hash(video_hash)
        if self.get_video(normalized_hash) is None:
            return None
        share_id = uuid.uuid4().hex
        now = utc_now()
        with self._connect() as connection:
            connection.execute(
                """
                INSERT INTO dub_shares (share_id, video_hash, title, user_id, created_at)
                VALUES (?, ?, ?, ?, ?)
                """,
                (share_id, normalized_hash, title[:200], user_id, now),
            )
            connection.executemany(
                """
                INSERT INTO dub_share_segments (
                    share_id, sentence_index, start_ms, end_ms, text,
                    audio_filename, audio_size_bytes, overall_score, recognized_text,
                    score_details
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                [
                    (
                        share_id,
                        segment["sentence_index"],
                        segment["start_ms"],
                        segment["end_ms"],
                        segment["text"],
                        segment["audio_filename"],
                        segment["audio_size_bytes"],
                        segment.get("overall_score", 0),
                        segment.get("recognized_text", ""),
                        segment.get("score_details", "{}"),
                    )
                    for segment in segments
                ],
            )
            return {
                "share_id": share_id,
                "video_hash": normalized_hash,
                "title": title[:200],
                "created_at": now,
            }

    def get_dub_share(self, share_id: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            share = connection.execute(
                "SELECT * FROM dub_shares WHERE share_id = ?", (share_id,)
            ).fetchone()
            if share is None:
                return None
            rows = connection.execute(
                """
                SELECT * FROM dub_share_segments
                WHERE share_id = ?
                ORDER BY sentence_index
                """,
                (share_id,),
            ).fetchall()
        result = dict(share)
        result["segments"] = [dict(row) for row in rows]
        return result

    def record_attempt(
        self,
        *,
        attempt_id: str,
        video_hash: str,
        sentence_index: int,
        result: Dict[str, Any],
        audio_filename: Optional[str],
        user_id: str,
    ) -> None:
        with self._connect() as connection:
            connection.execute(
                """
                INSERT INTO attempts (
                    attempt_id, video_hash, sentence_index, result_json,
                    audio_filename, user_id, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    attempt_id,
                    normalize_video_hash(video_hash),
                    sentence_index,
                    json.dumps(result, ensure_ascii=False),
                    audio_filename,
                    user_id,
                    utc_now(),
                ),
            )

    def create_user(
        self,
        *,
        username: str,
        password: str,
        nickname: Optional[str] = None,
        phone: Optional[str] = None,
    ) -> Optional[Dict[str, Any]]:
        user_id = uuid.uuid4().hex
        now = utc_now()
        try:
            with self._connect() as connection:
                row = connection.execute(
                    """
                    INSERT INTO users (id, username, nickname, phone, password_hash, created_at)
                    VALUES (?, ?, ?, ?, ?, ?)
                    """,
                    (
                        user_id,
                        username.strip().lower(),
                        (nickname or username).strip()[:80],
                        (phone or "").strip()[:20] or None,
                        hash_password(password),
                        now,
                    ),
                ).fetchone()
        except sqlite3.IntegrityError:
            return None
        return self.get_user_by_id(user_id)

    def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            row = connection.execute(
                "SELECT * FROM users WHERE username = ? COLLATE NOCASE",
                (username.strip(),),
            ).fetchone()
        return dict(row) if row else None

    def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            row = connection.execute(
                "SELECT id, username, nickname, phone, created_at FROM users WHERE id = ?",
                (user_id,),
            ).fetchone()
        return dict(row) if row else None

    def authenticate_user(self, username: str, password: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            row = connection.execute(
                "SELECT * FROM users WHERE username = ? COLLATE NOCASE",
                (username.strip(),),
            ).fetchone()
        if row is None or not verify_password(password, row["password_hash"]):
            return None
        return self.get_user_by_id(row["id"])

    def create_session(self, user_id: str) -> str:
        token = new_token()
        now = datetime.now(timezone.utc)
        with self._connect() as connection:
            connection.execute(
                """
                INSERT INTO auth_tokens (token_hash, user_id, created_at, expires_at)
                VALUES (?, ?, ?, ?)
                """,
                (
                    hash_token(token),
                    user_id,
                    now.isoformat(),
                    (now + timedelta(days=30)).isoformat(),
                ),
            )
        return token

    def resolve_session(self, token: str) -> Optional[Dict[str, Any]]:
        with self._connect() as connection:
            row = connection.execute(
                """
                SELECT u.id, u.username, u.nickname, u.created_at
                FROM auth_tokens t JOIN users u ON u.id = t.user_id
                WHERE t.token_hash = ? AND t.expires_at > ?
                """,
                (hash_token(token), datetime.now(timezone.utc).isoformat()),
            ).fetchone()
        return dict(row) if row else None

    def delete_session(self, token: str) -> None:
        with self._connect() as connection:
            connection.execute(
                "DELETE FROM auth_tokens WHERE token_hash = ?",
                (hash_token(token),),
            )

    def list_courses(self, user_id: str) -> List[Dict[str, Any]]:
        with self._connect() as connection:
            rows = connection.execute(
                """
                SELECT v.*, COUNT(s.sentence_index) AS sentence_count,
                       CASE WHEN e.user_id IS NULL THEN 0 ELSE 1 END AS enrolled
                FROM videos v
                LEFT JOIN sentences s ON s.video_hash = v.video_hash
                LEFT JOIN enrollments e ON e.video_hash = v.video_hash AND e.user_id = ?
                WHERE v.status = 'ready'
                GROUP BY v.video_hash
                ORDER BY e.created_at DESC, v.created_at DESC
                """,
                (user_id,),
            ).fetchall()
        return [dict(row) for row in rows]

    def enroll_course(self, user_id: str, video_hash: str) -> bool:
        normalized_hash = normalize_video_hash(video_hash)
        with self._connect() as connection:
            video = connection.execute(
                "SELECT 1 FROM videos WHERE video_hash = ? AND status = 'ready'",
                (normalized_hash,),
            ).fetchone()
            if video is None:
                return False
            connection.execute(
                """
                INSERT OR IGNORE INTO enrollments (user_id, video_hash, created_at)
                VALUES (?, ?, ?)
                """,
                (user_id, normalized_hash, utc_now()),
            )
        return True

    def unenroll_course(self, user_id: str, video_hash: str) -> bool:
        normalized_hash = normalize_video_hash(video_hash)
        with self._connect() as connection:
            cursor = connection.execute(
                "DELETE FROM enrollments WHERE user_id = ? AND video_hash = ?",
                (user_id, normalized_hash),
            )
        return cursor.rowcount > 0

    def list_user_results(self, user_id: str) -> List[Dict[str, Any]]:
        with self._connect() as connection:
            rows = connection.execute(
                """
                SELECT a.attempt_id, a.video_hash, a.sentence_index, a.result_json, a.created_at,
                       v.title AS course_title, s.text AS sentence_text
                FROM attempts a
                JOIN videos v ON v.video_hash = a.video_hash
                LEFT JOIN sentences s
                    ON s.video_hash = a.video_hash AND s.sentence_index = a.sentence_index
                WHERE a.user_id = ?
                ORDER BY a.created_at DESC
                LIMIT 500
                """,
                (user_id,),
            ).fetchall()
        results = []
        for row in rows:
            payload = json.loads(row["result_json"] or "{}")
            results.append(
                {
                    "attempt_id": row["attempt_id"],
                    "video_hash": row["video_hash"],
                    "course_title": row["course_title"],
                    "sentence_index": row["sentence_index"],
                    "sentence_text": row["sentence_text"] or "",
                    "overall_score": float(payload.get("overall_score", 0)),
                    "passed": bool(payload.get("passed", False)),
                    "created_at": row["created_at"],
                }
            )
        return results

    def list_user_dub_shares(self, user_id: str) -> List[Dict[str, Any]]:
        with self._connect() as connection:
            rows = connection.execute(
                """
                SELECT d.share_id, d.video_hash, d.title, d.created_at,
                       v.title AS course_title,
                       COUNT(g.id) AS segment_count,
                       COALESCE(AVG(NULLIF(g.overall_score, 0)), 0) AS average_score
                FROM dub_shares d
                JOIN videos v ON v.video_hash = d.video_hash
                LEFT JOIN dub_share_segments g ON g.share_id = d.share_id
                WHERE d.user_id = ?
                GROUP BY d.share_id
                ORDER BY d.created_at DESC
                LIMIT 500
                """,
                (user_id,),
            ).fetchall()
        return [dict(row) for row in rows]

    def delete_video(self, video_hash: str) -> Optional[Dict[str, Any]]:
        video = self.get_video(video_hash)
        if video is None:
            return None
        with self._connect() as connection:
            connection.execute(
                "DELETE FROM videos WHERE video_hash = ?",
                (normalize_video_hash(video_hash),),
            )
        return video
