"""YT Cut - 유튜브 구간 잘라 받기.

GET  /          폼 + 서버 파일 목록(받기/삭제)
POST /cut       yt-dlp로 지정한 시작~끝 구간만 mp4로 잘라 /downloads 에 저장
GET  /download  저장된 파일을 브라우저로 내려받기
POST /delete    저장된 파일 삭제
"""

import html
import os
import re
import subprocess
from datetime import datetime
from urllib.parse import quote

from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response

app = FastAPI(title="ClipCut")

DOWNLOAD_DIR = "/downloads"
# MM:SS 또는 HH:MM:SS 만 허용 (shell 인자로 넘기기 전 검증)
TIME_RE = re.compile(r"^(?:\d{1,2}:)?\d{1,2}:[0-5]\d$")
TIMEOUT_SEC = 1800  # 30분


# ── 인라인 SVG 아이콘 (currentColor 사용) ────────────────────────────
def _svg(body: str) -> str:
    return ('<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" '
            'stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" '
            f'aria-hidden="true">{body}</svg>')


IC_FILM = _svg('<rect x="3" y="4" width="18" height="16" rx="2.2"/>'
               '<path d="M7 4v16M17 4v16M3 9h4M3 15h4M17 9h4M17 15h4"/>')
IC_SCISSORS = _svg('<circle cx="6" cy="6" r="2.6"/><circle cx="6" cy="18" r="2.6"/>'
                   '<path d="M20 4 8.1 15.9M14.5 14.5 20 20M8.1 8.1 12 12"/>')
IC_LINK = _svg('<path d="M9.5 14.5 14.5 9.5"/>'
               '<path d="M11.5 7.3 13 5.8a3.7 3.7 0 0 1 5.2 5.2l-1.5 1.5"/>'
               '<path d="M12.5 16.7 11 18.2a3.7 3.7 0 0 1-5.2-5.2l1.5-1.5"/>')
IC_DOWNLOAD = _svg('<path d="M12 3.5v11"/><path d="m7.5 10.5 4.5 4.5 4.5-4.5"/>'
                   '<path d="M5 20.5h14"/>')
IC_TRASH = _svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/>'
                '<path d="M6.5 7 7.5 20a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1L17.5 7"/>'
                '<path d="M9.5 7V4.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V7"/>')
IC_CLOCK = _svg('<circle cx="12" cy="12" r="8.4"/><path d="M12 7.5V12l3 1.8"/>')


def _safe_name(name: str) -> str:
    """파일명만 허용(경로 탈출 차단)하고 DOWNLOAD_DIR 내부 실제 경로를 반환."""
    if "/" in name or "\\" in name or name in ("", ".", ".."):
        raise HTTPException(status_code=400, detail="잘못된 파일명")
    path = os.path.join(DOWNLOAD_DIR, name)
    base = os.path.realpath(DOWNLOAD_DIR)
    if not os.path.realpath(path).startswith(base + os.sep):
        raise HTTPException(status_code=400, detail="잘못된 경로")
    return path


def _human(n: int) -> str:
    size = float(n)
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024 or unit == "GB":
            return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} GB"


def _list_files():
    try:
        names = os.listdir(DOWNLOAD_DIR)
    except OSError:
        return []
    items = []
    for name in names:
        if name.startswith("."):  # .metube 등 숨김 항목 제외
            continue
        path = os.path.join(DOWNLOAD_DIR, name)
        if os.path.isfile(path):
            items.append((name, os.path.getsize(path), os.path.getmtime(path)))
    items.sort(key=lambda e: e[2], reverse=True)  # 최신순
    return items


def _file_list_html() -> str:
    items = _list_files()
    if not items:
        body = (f'<div class="empty">{IC_FILM}'
                '<p>아직 자른 클립이 없어요.<br>위에서 첫 구간을 잘라보세요.</p></div>')
    else:
        rows = []
        for name, size, mtime in items:
            esc = html.escape(name)
            href = "download?name=" + quote(name)
            date = datetime.fromtimestamp(mtime).strftime("%y.%m.%d %H:%M")
            rows.append(
                '<li class="file">'
                f'<span class="file-ic">{IC_FILM}</span>'
                '<span class="file-meta">'
                f'<span class="file-name" title="{esc}">{esc}</span>'
                f'<span class="file-sub">{_human(size)} · {date}</span>'
                '</span>'
                f'<a class="iconbtn dl" href="{html.escape(href)}" '
                f'aria-label="받기" title="받기">{IC_DOWNLOAD}</a>'
                '<form class="inline" method="post" action="delete" '
                'onsubmit="return confirm(\'이 파일을 삭제할까요?\')">'
                f'<input type="hidden" name="name" value="{esc}">'
                '<button class="iconbtn del" type="submit" '
                f'aria-label="삭제" title="삭제">{IC_TRASH}</button>'
                '</form>'
                '</li>'
            )
        body = '<ul class="files">' + "".join(rows) + '</ul>'
    return (
        '<section class="files-sec">'
        '<div class="files-head"><h2>파일</h2>'
        f'<span class="count">{len(items)}개</span></div>'
        f'{body}</section>'
    )


STYLE = """
*{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#17120E; --bg-soft:#1f1813; --panel:#241c16; --panel-2:#2c2219;
  --line:#3a2d22; --line-soft:#2c2219;
  --text:#F2EBE2; --muted:#A99C8F; --faint:#7C7064;
  --accent:#FF5C39; --accent-soft:#ff5c3922; --accent-ink:#2a0d04;
  --ok-bd:#3f7a4e; --ok-bg:#15321f; --ok-tx:#9fe6b4;
  --err-bd:#7a3a35; --err-bg:#341b18; --err-tx:#f3b0aa;
  --danger:#E0584E;
}
html{-webkit-text-size-adjust:100%}
body{font-family:Pretendard,system-ui,sans-serif;color:var(--text);
  background:
    radial-gradient(900px 380px at 50% -120px, #ff5c3914, transparent 70%),
    var(--bg);
  min-height:100vh;line-height:1.55;
  padding:max(24px,env(safe-area-inset-top)) 16px 64px;
  -webkit-font-smoothing:antialiased;}
.wrap{max-width:560px;margin:0 auto}
.mono{font-family:'JetBrains Mono',ui-monospace,monospace;font-variant-numeric:tabular-nums}

/* header */
.brand{display:flex;align-items:center;gap:13px;margin:6px 2px 22px;
  animation:rise .5s .02s both}
.brand .logo{width:46px;height:46px;border-radius:13px;display:grid;place-items:center;
  background:linear-gradient(150deg,var(--panel-2),var(--panel));
  border:1px solid var(--line);color:var(--accent);flex:none;
  box-shadow:inset 0 1px 0 #ffffff10}
.brand .logo svg{width:25px;height:25px}
.brand h1{font-size:20px;font-weight:800;letter-spacing:-.01em}
.brand h1 .ac{color:var(--accent)}
.brand .sub{font-size:13px;font-weight:500;color:var(--muted);margin-top:1px}

.card{background:linear-gradient(180deg,var(--panel),var(--bg-soft));
  border:1px solid var(--line);border-radius:20px;padding:20px;
  box-shadow:0 30px 70px -34px #000c, inset 0 1px 0 #ffffff08;
  animation:rise .5s .06s both}

/* film-strip timeline signature */
.timeline{margin:2px 0 20px}
.tl-track{position:relative;height:48px;border-radius:9px;overflow:hidden;
  background:linear-gradient(180deg,#1c1510,#171008);border:1px solid var(--line)}
.tl-track::before,.tl-track::after{content:"";position:absolute;left:0;right:0;height:8px;
  background-image:repeating-linear-gradient(90deg,#0c0805 0 9px,transparent 9px 20px)}
.tl-track::before{top:5px}.tl-track::after{bottom:5px}
.tl-seg{position:absolute;top:0;bottom:0;left:30%;right:30%;
  background:linear-gradient(180deg,var(--accent-soft),#ff5c3910);
  border-left:2px solid var(--accent);border-right:2px solid var(--accent)}
.tl-seg::before{content:"";position:absolute;inset:0;margin:auto;width:26px;height:26px;
  border-radius:50%;background:#1a120c;border:1px solid var(--accent);
  display:grid;place-items:center}
.tl-cut{position:absolute;inset:0;margin:auto;width:16px;height:16px;color:var(--accent);
  z-index:1}
.tl-marks{display:flex;align-items:flex-end;justify-content:space-between;margin-top:11px}
.tl-pt{display:flex;flex-direction:column;gap:2px}
.tl-pt.out{align-items:flex-end}
.tl-pt b{font-size:10px;font-weight:700;letter-spacing:.16em;color:var(--faint)}
.tl-pt time{font-size:19px;font-weight:600;color:var(--text)}

/* fields */
.field{margin-top:15px}
.field>label{display:block;font-size:12px;font-weight:600;letter-spacing:.02em;
  color:var(--muted);margin-bottom:7px}
.input-wrap{display:flex;align-items:center;gap:10px;background:var(--bg-soft);
  border:1px solid var(--line);border-radius:12px;padding:0 13px;
  transition:border-color .15s,box-shadow .15s}
.input-wrap:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.input-wrap>svg{width:18px;height:18px;color:var(--faint);flex:none}
.input-wrap input{flex:1;min-width:0;background:none;border:0;outline:0;color:var(--text);
  font-family:inherit;font-size:15px;padding:13px 0}
.input-wrap input::placeholder{color:var(--faint)}
.tc-row{display:flex;gap:12px}
.tc-row .field{flex:1;margin-top:15px}
.tc input{font-family:'JetBrains Mono',monospace;font-size:15px;letter-spacing:.03em}
.help{font-size:12px;color:var(--faint);margin-top:9px;display:flex;gap:6px;align-items:center}
.help svg{width:14px;height:14px;flex:none}

/* primary cut button */
.cut-btn{width:100%;margin-top:20px;display:flex;gap:9px;align-items:center;justify-content:center;
  padding:15px;font-family:inherit;font-size:16px;font-weight:700;color:var(--accent-ink);
  background:var(--accent);border:0;border-radius:13px;cursor:pointer;
  box-shadow:0 8px 24px -8px #ff5c3977;
  transition:filter .15s,box-shadow .2s,transform .1s}
.cut-btn:hover{filter:brightness(1.06);box-shadow:0 12px 30px -9px #ff5c3999}
.cut-btn:active{transform:translateY(1px)}
.cut-btn svg{width:20px;height:20px}
.cut-btn .spin{display:none;width:18px;height:18px;border-radius:50%;
  border:2.5px solid #2a0d0455;border-top-color:var(--accent-ink);animation:spin .7s linear infinite}
.cut-btn.loading .ic{display:none}
.cut-btn.loading .spin{display:block}

/* result banner */
.banner{margin-top:18px;padding:14px 16px;border-radius:13px;border:1px solid;
  display:flex;flex-direction:column;gap:10px;animation:rise .35s both}
.banner.ok{border-color:var(--ok-bd);background:var(--ok-bg)}
.banner.err{border-color:var(--err-bd);background:var(--err-bg)}
.banner .msg{font-size:14px;font-weight:600;white-space:pre-wrap;word-break:break-all}
.banner.ok .msg{color:var(--ok-tx)}
.banner.err .msg{color:var(--err-tx)}
.dl-big{align-self:flex-start;display:inline-flex;align-items:center;gap:8px;
  padding:11px 17px;border-radius:11px;background:var(--accent);color:var(--accent-ink);
  font-weight:700;font-size:14px;text-decoration:none;
  box-shadow:0 8px 22px -10px #ff5c3988;transition:filter .15s,transform .1s}
.dl-big:hover{filter:brightness(1.06)} .dl-big:active{transform:translateY(1px)}
.dl-big svg{width:18px;height:18px}
.dl-name{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--ok-tx);
  opacity:.8;word-break:break-all}

/* file list */
.files-sec{margin-top:26px;animation:rise .5s .1s both}
.files-head{display:flex;align-items:baseline;justify-content:space-between;
  padding:0 2px 4px}
.files-head h2{font-size:15px;font-weight:700}
.files-head .count{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--muted)}
.files{list-style:none}
.file{display:flex;align-items:center;gap:12px;padding:12px 6px;
  border-top:1px solid var(--line-soft)}
.file:first-child{border-top:0}
.file-ic{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;
  background:var(--bg-soft);border:1px solid var(--line);color:var(--faint);flex:none}
.file-ic svg{width:19px;height:19px}
.file-meta{flex:1;min-width:0}
.file-name{display:block;font-size:14px;font-weight:500;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.file-sub{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--faint);margin-top:3px}
.inline{display:inline;margin:0}
.iconbtn{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:11px;
  border:1px solid var(--line);background:var(--bg-soft);color:var(--muted);cursor:pointer;
  transition:.15s}
.iconbtn svg{width:18px;height:18px}
.iconbtn.dl:hover{color:var(--accent);border-color:var(--accent)}
.iconbtn.del:hover{color:#fff;background:var(--danger);border-color:var(--danger)}
.empty{display:flex;flex-direction:column;align-items:center;gap:12px;
  padding:36px 0 30px;color:var(--faint);text-align:center;font-size:13.5px}
.empty svg{width:34px;height:34px;opacity:.7}

/* loading overlay */
.overlay{position:fixed;inset:0;display:none;flex-direction:column;gap:16px;
  align-items:center;justify-content:center;z-index:50;text-align:center;
  background:rgba(15,11,8,.72);backdrop-filter:blur(6px)}
.overlay.show{display:flex}
.overlay .sp{width:42px;height:42px;border-radius:50%;
  border:3px solid #ffffff1f;border-top-color:var(--accent);animation:spin .8s linear infinite}
.overlay p{font-size:14px;font-weight:600;color:var(--text)}
.overlay small{display:block;color:var(--muted);font-weight:400;margin-top:3px}

a,button{-webkit-tap-highlight-color:transparent}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
}
"""

SCRIPT = """
(function(){
  var s=document.getElementById('start'), e=document.getElementById('end');
  var ti=document.getElementById('tcIn'), to=document.getElementById('tcOut');
  function up(){ if(ti) ti.textContent=(s.value.trim()||'--:--');
                if(to) to.textContent=(e.value.trim()||'--:--'); }
  if(s&&e){ s.addEventListener('input',up); e.addEventListener('input',up); }
  var f=document.getElementById('cutForm'), ov=document.getElementById('overlay'),
      b=document.getElementById('cutBtn');
  if(f){ f.addEventListener('submit',function(){
    if(b) b.classList.add('loading'); if(ov) ov.classList.add('show'); }); }
})();
"""

HEAD = """<!doctype html><html lang="ko"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#17120E">
<title>ClipCut · 유튜브 구간 잘라받기</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap">
<style>__STYLE__</style></head>"""


def page(message: str = "", ok: bool = False, url: str = "",
         start: str = "", end: str = "", download_name: str = "") -> str:
    banner = ""
    if message:
        cls = "ok" if ok else "err"
        dl = ""
        if download_name:
            href = "download?name=" + quote(download_name)
            dl = (f'<a class="dl-big" href="{html.escape(href)}">{IC_DOWNLOAD}파일 받기</a>'
                  f'<span class="dl-name">{html.escape(download_name)}</span>')
        banner = (f'<div class="banner {cls}"><span class="msg">{html.escape(message)}</span>'
                  f'{dl}</div>')

    tc_in = html.escape(start) if start else "03:30"
    tc_out = html.escape(end) if end else "06:20"

    head = HEAD.replace("__STYLE__", STYLE)
    return f"""{head}
<body>
<div class="overlay" id="overlay">
  <div class="sp"></div>
  <p>구간을 자르는 중…<small>영상 길이에 따라 몇 초~몇 분 걸려요</small></p>
</div>
<div class="wrap">
  <header class="brand">
    <span class="logo">{IC_FILM}</span>
    <div>
      <h1>Clip<span class="ac">Cut</span></h1>
      <div class="sub">유튜브 구간을 잘라 바로 받기</div>
    </div>
  </header>

  <main class="card">
    <div class="timeline" aria-hidden="true">
      <div class="tl-track">
        <span class="tl-seg"></span>
        <span class="tl-cut">{IC_SCISSORS}</span>
      </div>
      <div class="tl-marks">
        <span class="tl-pt"><b>IN</b><time class="mono" id="tcIn">{tc_in}</time></span>
        <span class="tl-pt out"><b>OUT</b><time class="mono" id="tcOut">{tc_out}</time></span>
      </div>
    </div>

    {banner}

    <form method="post" action="cut" id="cutForm">
      <div class="field">
        <label for="url">유튜브 URL</label>
        <div class="input-wrap">{IC_LINK}
          <input id="url" name="url" type="url" required
                 placeholder="https://www.youtube.com/watch?v=…" value="{html.escape(url)}">
        </div>
      </div>
      <div class="tc-row">
        <div class="field tc">
          <label for="start">시작</label>
          <div class="input-wrap">{IC_CLOCK}
            <input id="start" name="start" required placeholder="03:30"
                   inputmode="numeric" value="{html.escape(start)}">
          </div>
        </div>
        <div class="field tc">
          <label for="end">끝</label>
          <div class="input-wrap">{IC_CLOCK}
            <input id="end" name="end" required placeholder="06:20"
                   inputmode="numeric" value="{html.escape(end)}">
          </div>
        </div>
      </div>
      <p class="help">{IC_CLOCK}형식: 분:초(03:30) 또는 시:분:초(01:03:30)</p>
      <button class="cut-btn" id="cutBtn" type="submit">
        <span class="ic">{IC_SCISSORS}</span><span class="spin"></span>잘라서 받기
      </button>
    </form>
  </main>

  {_file_list_html()}
</div>
<script>{SCRIPT}</script>
</body></html>"""


FAVICON = (
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
    '<rect width="24" height="24" rx="6" fill="#17120E"/>'
    '<g fill="none" stroke="#FF5C39" stroke-width="1.7" stroke-linecap="round" '
    'stroke-linejoin="round"><circle cx="7" cy="7" r="2.2"/><circle cx="7" cy="17" r="2.2"/>'
    '<path d="M19 5 8.5 15.5M14 14l5 5M8.5 8.5 12 12"/></g></svg>'
)


@app.get("/favicon.svg")
@app.get("/favicon.ico")
def favicon() -> Response:
    return Response(FAVICON, media_type="image/svg+xml")


@app.get("/", response_class=HTMLResponse)
def index() -> str:
    return page()


@app.post("/cut", response_class=HTMLResponse)
def cut(url: str = Form(...), start: str = Form(...), end: str = Form(...)) -> str:
    url, start, end = url.strip(), start.strip(), end.strip()

    if not (url.startswith("http://") or url.startswith("https://")):
        return page("올바른 URL이 아닙니다.", url=url, start=start, end=end)
    if not TIME_RE.match(start) or not TIME_RE.match(end):
        return page("시간 형식이 올바르지 않습니다. 예) 03:30 또는 01:03:30",
                    url=url, start=start, end=end)

    section = f"*{start}-{end}"
    suffix = f"{start}-{end}".replace(":", "")
    outtmpl = f"{DOWNLOAD_DIR}/%(title)s_{suffix}.%(ext)s"

    cmd = [
        "yt-dlp",
        "--download-sections", section,
        "-f", "bv*+ba/b",
        "--merge-output-format", "mp4",
        "--no-playlist",
        "--no-simulate", "--print", "after_move:filepath",  # 최종 저장 경로 출력
        "-o", outtmpl,
        url,
    ]

    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT_SEC)
    except subprocess.TimeoutExpired:
        return page(f"처리 시간이 초과됐습니다(>{TIMEOUT_SEC // 60}분).",
                    url=url, start=start, end=end)

    if proc.returncode != 0:
        tail = (proc.stderr or proc.stdout or "").strip()[-1500:]
        return page(f"실패했습니다.\n\n{tail}", url=url, start=start, end=end)

    final_path = ""
    for line in (proc.stdout or "").splitlines():
        line = line.strip()
        if line.startswith(DOWNLOAD_DIR + "/"):
            final_path = line
    download_name = os.path.basename(final_path) if final_path else ""

    return page(f"완료! {start} ~ {end} 구간을 잘랐어요.", ok=True,
                download_name=download_name)


@app.get("/download")
def download(name: str) -> FileResponse:
    path = _safe_name(name)
    if not os.path.isfile(path):
        raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
    return FileResponse(path, filename=name, media_type="application/octet-stream")


@app.post("/delete")
def delete(name: str = Form(...)) -> RedirectResponse:
    path = _safe_name(name)
    if os.path.isfile(path):
        os.remove(path)
    return RedirectResponse("/", status_code=303)
