from __future__ import annotations

import hashlib
import json
import re
import uuid
from pathlib import Path
from typing import Any

from fastapi import HTTPException
from sqlmodel import Session, select

from app.core.config import get_settings
from app.models.core import (
    ActivityEvent,
    PlatformAccount,
    PostDraft,
    Provider,
    PublishJob,
    JobStatus,
    UploadRequest,
    VideoAsset,
    VideoStatus,
    WebsiteCompanion,
    YouTubeUploadAudit,
    now_utc,
)
from app.security.redaction import redact_json
from app.services.post_draft_mutations import apply_draft_patch
from app.services.youtube_upload import YOUTUBE_UPLOAD_SCOPE, youtube_url

HASHTAG_RE = re.compile(r"#[a-z0-9_]+", re.IGNORECASE)


def normalize_hashtag(tag: str) -> str | None:
    value = str(tag or "").strip().lstrip("#")
    value = re.sub(r"[^A-Za-z0-9_]", "", value)
    if not value:
        return None
    return f"#{value.lower()}"


def build_youtube_description(description: str | None, tags: list[str] | None, *, append_hashtags: bool = True) -> str:
    text = (description or "").strip()
    if not append_hashtags:
        return text
    existing = {item.lower() for item in HASHTAG_RE.findall(text)}
    ordered: list[str] = []
    for tag in tags or []:
        normalized = normalize_hashtag(tag)
        if normalized and normalized.lower() not in existing and normalized not in ordered:
            ordered.append(normalized)
    if not ordered:
        return text
    suffix = " ".join(ordered)
    return f"{text}\n\n{suffix}" if text else suffix


def _resolve_video_path(video: VideoAsset) -> Path:
    path = Path(video.file_path or "").expanduser()
    if not path.is_absolute():
        path = Path(__file__).resolve().parents[2] / path
    if not str(video.file_path or "").strip() or not path.exists() or not path.is_file():
        raise HTTPException(status_code=400, detail="Video file missing")
    return path


def _sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def _synthetic_media(video: VideoAsset, draft: PostDraft) -> bool:
    disclosure = (draft.metadata_json or {}).get("disclosure") if isinstance(draft.metadata_json, dict) else {}
    if isinstance(disclosure, dict) and "synthetic_media" in disclosure:
        return bool(disclosure["synthetic_media"])
    return bool(video.is_aigc)


def _dry_run_account(session: Session) -> PlatformAccount:
    account = session.exec(select(PlatformAccount).where(PlatformAccount.provider == Provider.youtube, PlatformAccount.external_account_id == "existing-jarvis-dry-run")).first()
    if not account:
        account = PlatformAccount(
            provider=Provider.youtube,
            display_name="Existing Jarvis uploader dry-run",
            external_account_id="existing-jarvis-dry-run",
            account_type="dry_run",
            scopes=[YOUTUBE_UPLOAD_SCOPE],
            status="dry_run",
        )
        session.add(account)
        session.commit()
        session.refresh(account)
    return account


def prepare_upload_snapshot(session: Session, post_draft_id: str, *, approved_by: str = "creator") -> dict[str, Any]:
    draft = session.get(PostDraft, post_draft_id)
    if not draft or draft.provider != Provider.youtube:
        raise HTTPException(status_code=404, detail="YouTube draft not found")
    video = session.get(VideoAsset, draft.video_asset_id)
    if not video:
        raise HTTPException(status_code=404, detail="Video not found")
    if (draft.privacy_status or "private") != "private":
        raise HTTPException(status_code=400, detail="Only private YouTube uploads are allowed")
    if not draft.title:
        raise HTTPException(status_code=400, detail="YouTube title is required")
    if not draft.description:
        raise HTTPException(status_code=400, detail="YouTube description is required")
    video_path = _resolve_video_path(video)
    actual_sha = _sha256(video_path)
    # Prepared packages may carry a trusted package checksum from import. The upload snapshot records the actual file hash.
    tags = list(draft.tags_json or draft.hashtags_json or [])
    final_description = build_youtube_description(draft.description, tags, append_hashtags=True)
    package_id = video.package_id or video.candidate_id
    return {
        "video_asset_id": video.id,
        "post_draft_id": draft.id,
        "package_id": package_id,
        "candidate_id": video.candidate_id,
        "file_path": str(video_path),
        "sha256": actual_sha,
        "sha_prefix": actual_sha[:12],
        "youtube_title": draft.title,
        "youtube_description_source": draft.description,
        "youtube_description_final": final_description,
        "youtube_tags": tags,
        "privacy_status": "private",
        "synthetic_media": _synthetic_media(video, draft),
        "category_id": draft.category_id or "22",
        "approved_at": now_utc().isoformat(),
        "approved_by": approved_by,
        "append_hashtags_to_youtube_description": True,
        "executor_type": "existing_jarvis_uploader",
    }


def _outbox_root() -> Path:
    return get_settings().storage_root / "outbox" / "upload_requests"


def approve_upload_request(session: Session, post_draft_id: str, *, approved_by: str = "creator") -> dict[str, Any]:
    snapshot = prepare_upload_snapshot(session, post_draft_id, approved_by=approved_by)
    # supersede older open requests for this draft
    old_rows = session.exec(select(UploadRequest).where(UploadRequest.post_draft_id == post_draft_id, UploadRequest.status.in_(["approved", "sent_to_executor"]))).all()
    for row in old_rows:
        row.status = "invalidated"
        row.error = "superseded by newer upload approval"
        row.updated_at = now_utc()
        session.add(row)
    request = UploadRequest(
        video_asset_id=snapshot["video_asset_id"],
        post_draft_id=post_draft_id,
        executor_type="existing_jarvis_uploader",
        status="approved",
        approval_text="Creator approved private upload via Dashboard Upload Orchestrator.",
        approval_command=f"APPROVE_UPLOAD {snapshot['package_id']} {snapshot['sha_prefix']}",
        expected_candidate_id=snapshot["package_id"],
        expected_video_sha=snapshot["sha_prefix"],
        expected_privacy="private",
        approved_at=now_utc(),
        metadata_snapshot_json=snapshot,
    )
    session.add(request)
    session.commit()
    session.refresh(request)
    outbox = _outbox_root()
    outbox.mkdir(parents=True, exist_ok=True)
    outbox_payload = {
        "request_id": request.id,
        "workflow": "dashboard_review_to_existing_jarvis_uploader",
        "executor_type": request.executor_type,
        "status": request.status,
        "approval_command": request.approval_command,
        "upload_snapshot": snapshot,
    }
    outbox_path = outbox / f"{request.id}.json"
    outbox_path.write_text(json.dumps(redact_json(outbox_payload), indent=2), encoding="utf-8")
    request.outbox_path = str(outbox_path)
    request.updated_at = now_utc()
    session.add(request)
    session.add(ActivityEvent(
        entity_type="upload_request",
        entity_id=request.id,
        event_type="upload_request_approved",
        label="Private YouTube upload snapshot approved",
        payload_json={"package_id": snapshot["package_id"], "outbox_path": str(outbox_path)},
        created_by=approved_by,
    ))
    session.commit()
    session.refresh(request)
    data = request.model_dump(mode="json")
    data["upload_snapshot"] = snapshot
    return data


def invalidate_open_requests_for_draft(session: Session, post_draft_id: str, *, reason: str = "metadata changed after approval") -> None:
    rows = session.exec(select(UploadRequest).where(UploadRequest.post_draft_id == post_draft_id, UploadRequest.status.in_(["approved", "sent_to_executor"]))).all()
    for req in rows:
        req.status = "invalidated"
        req.error = reason
        req.updated_at = now_utc()
        session.add(req)
        session.add(ActivityEvent(
            entity_type="upload_request",
            entity_id=req.id,
            event_type="upload_request_invalidated",
            label="Upload approval invalidated",
            payload_json={"reason": reason},
        ))


def execute_existing_jarvis_upload(session: Session, upload_request_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    payload = payload or {}
    req = session.get(UploadRequest, upload_request_id)
    if not req:
        raise HTTPException(status_code=404, detail="UploadRequest not found")
    if req.status != "approved":
        raise HTTPException(status_code=409, detail=f"UploadRequest is not approved: {req.status}")
    snapshot = dict(req.metadata_snapshot_json or {})
    if not snapshot:
        raise HTTPException(status_code=400, detail="Upload snapshot missing")
    if snapshot.get("privacy_status") != "private":
        raise HTTPException(status_code=400, detail="Only private uploads are allowed")
    dry_run = bool(payload.get("dry_run", True))
    if not dry_run:
        raise HTTPException(status_code=409, detail="Real existing Jarvis upload execution is disabled in tests; use the agent executor with the approved snapshot.")
    account = _dry_run_account(session)
    video = session.get(VideoAsset, req.video_asset_id)
    draft = session.get(PostDraft, req.post_draft_id)
    if not video or not draft:
        raise HTTPException(status_code=404, detail="Video or draft missing")
    body = {
        "snippet": {
            "title": snapshot["youtube_title"],
            "description": snapshot["youtube_description_final"],
            "tags": snapshot.get("youtube_tags", []),
            "categoryId": snapshot.get("category_id") or "22",
        },
        "status": {
            "privacyStatus": "private",
            "selfDeclaredMadeForKids": False,
            "containsSyntheticMedia": bool(snapshot.get("synthetic_media", True)),
        },
    }
    job = PublishJob(post_draft_id=draft.id, provider=Provider.youtube, job_type="existing_jarvis_private_upload_dry_run", status=JobStatus.running, attempts=1, started_at=now_utc())
    session.add(job)
    session.commit()
    session.refresh(job)
    yt_id = "dryrun_" + uuid.uuid4().hex[:16]
    audit = YouTubeUploadAudit(
        video_asset_id=video.id,
        post_draft_id=draft.id,
        platform_account_id=account.id,
        publish_job_id=job.id,
        youtube_video_id=yt_id,
        youtube_url=youtube_url(yt_id),
        privacy_status="private",
        title=snapshot["youtube_title"],
        description_hash=hashlib.sha256(snapshot["youtube_description_final"].encode()).hexdigest(),
        tags_json=snapshot.get("youtube_tags", []),
        category_id=snapshot.get("category_id") or "22",
        contains_synthetic_media=bool(snapshot.get("synthetic_media", True)),
        self_declared_made_for_kids=False,
        notify_subscribers=False,
        upload_status="dry_run_succeeded",
        api_endpoints_called=["dry-run.youtube.videos.insert"],
        oauth_scopes_used=[YOUTUBE_UPLOAD_SCOPE],
        website_link_allowed=False,
        request_redacted_json=redact_json({"body": body, "video_path": Path(snapshot["file_path"]).name, "notifySubscribers": False, "dry_run": True}),
        response_redacted_json={"id": yt_id, "dry_run": True},
    )
    req.status = "completed"
    req.sent_at = req.sent_at or now_utc()
    req.completed_at = now_utc()
    req.youtube_video_id = yt_id
    req.upload_audit_id = audit.id
    req.updated_at = now_utc()
    draft.external_post_id = yt_id
    draft.status = "uploaded_private_dry_run"
    draft.privacy_status = "private"
    video.external_youtube_id = yt_id
    video.status = VideoStatus.uploaded_private
    companion = session.exec(select(WebsiteCompanion).where(WebsiteCompanion.video_asset_id == video.id)).first()
    if companion:
        companion.youtube_video_id = yt_id
        companion.video_url_internal = youtube_url(yt_id)
        companion.website_link_allowed = False
        companion.status = "ready_without_public_video"
        companion.updated_at = now_utc()
        session.add(companion)
    job.status = JobStatus.succeeded
    job.external_post_id = yt_id
    job.external_publish_id = yt_id
    job.finished_at = now_utc()
    session.add_all([audit, req, draft, video, job])
    session.add(ActivityEvent(entity_type="upload_request", entity_id=req.id, event_type="upload_request_completed", label="Existing Jarvis uploader dry-run completed", payload_json={"youtube_video_id": yt_id}))
    session.commit()
    session.refresh(audit)
    return {"ok": True, "dry_run": True, "youtube_video_id": yt_id, "youtube_url": youtube_url(yt_id), "audit": audit.model_dump(mode="json"), "upload_request": req.model_dump(mode="json")}


def save_and_approve(session: Session, post_draft_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    payload = payload or {}
    patch = payload.get("draft") or {}
    if patch:
        draft = session.get(PostDraft, post_draft_id)
        if not draft:
            raise HTTPException(status_code=404, detail="Draft not found")
        apply_draft_patch(session, draft, patch, invalidate_uploads=False)
        session.commit()
    return approve_upload_request(session, post_draft_id, approved_by=payload.get("approved_by") or "creator")
