"""Process storage ingest jobs — extract archives locally, upload to connector R2."""
from __future__ import annotations

import os
import shutil
import tempfile
from uuid import UUID

import structlog

from src.config import get_settings
from src.core.database import async_session_factory
from src.core.security.encryption import decrypt_value
from src.domain.connectors.repository import ConnectorRepository
from src.domain.storage_ingest.archive_paths import (
    apply_connection_prefix,
    connection_path_prefix,
    object_key_for_extracted_file,
    staging_abs_path,
    staging_file_is_visible,
)
from src.domain.storage_ingest.models import StorageIngestJobStatus
from src.domain.storage_ingest.repository import StorageIngestJobRepository
from src.integrations.archive_extract import extract_archive
from src.integrations.r2_connector_upload import upload_file_from_path

logger = structlog.get_logger()

_SKIP_DIR_NAMES = {"__MACOSX"}
_MAX_SKIP_LOG = 20


def _join_key(prefix: str, relative: str) -> str:
    rel = relative.replace("\\", "/").lstrip("/")
    base = prefix.strip().replace("\\", "/").strip("/")
    return f"{base}/{rel}" if base else rel


def _extract_prefix_from_object_key(object_key: str) -> str:
    key = object_key.replace("\\", "/").lstrip("/")
    if key.lower().endswith(".zip"):
        return key[:-4] or "extracted"
    return os.path.dirname(key).replace("\\", "/").strip("/")


def _connector_r2_credentials(conn) -> tuple[str, str, str]:
    config = dict(conn.config_json or {})
    secrets = dict(conn.secrets_enc or {})
    account_id = str(config.get("account_id") or "").strip()
    access_key = decrypt_value(secrets.get("access_key_id") or "")
    secret_key = decrypt_value(secrets.get("secret_access_key") or "")
    if not account_id or not access_key or not secret_key:
        raise RuntimeError("connector_r2_credentials_incomplete")
    return account_id, access_key, secret_key


async def process_storage_ingest_job(ctx: dict, job_id: str) -> dict:
    """Extract (if needed) and upload staged files to the destination R2 bucket."""
    db_factory = ctx.get("db_factory", async_session_factory)
    settings = get_settings()
    root = os.path.normpath(settings.storage_staging_upload_root)

    try:
        ingest_id = UUID(job_id)
    except ValueError:
        logger.warning("storage_ingest_invalid_job_id", job_id=job_id)
        return {"status": "invalid_job_id"}

    async with db_factory() as db:
        job_repo = StorageIngestJobRepository(db)
        job = await job_repo.get(ingest_id)
        if not job:
            return {"status": "not_found"}
        if job.status == StorageIngestJobStatus.COMPLETED.value:
            return {"status": "already_completed"}
        if job.status == StorageIngestJobStatus.RUNNING.value:
            return {"status": "already_running"}
        if job.status not in (
            StorageIngestJobStatus.PENDING.value,
            StorageIngestJobStatus.FAILED.value,
        ):
            return {"status": job.status}

        staging_relative_path = job.staging_relative_path
        bucket_name = job.bucket_name
        connection_id = job.connection_id
        object_key = job.object_key
        original_filename = job.original_filename
        extract_archive_flag = job.extract_archive
        archive_path_mode = job.archive_path_mode or "preserve_folders"

        if not staging_file_is_visible(root, staging_relative_path):
            logger.warning(
                "storage_ingest_staging_not_visible",
                job_id=job_id,
                path=staging_abs_path(root, staging_relative_path),
            )
            return {"status": "staging_not_visible"}

        claimed = await job_repo.try_claim_pending(ingest_id)
        await db.commit()
        if not claimed:
            return {"status": "claim_skipped"}

    staging_path = staging_abs_path(root, staging_relative_path)
    uploaded_keys: list[str] = []
    skipped: list[str] = []
    first_error: str | None = None

    try:
        conn = None
        account_id = access_key = secret_key = ""
        path_prefix = ""
        bucket = bucket_name

        async with db_factory() as db:
            conn = await ConnectorRepository(db).get_connection(connection_id)
            if not conn:
                raise RuntimeError("connector_not_found")
            account_id, access_key, secret_key = _connector_r2_credentials(conn)
            path_prefix = connection_path_prefix(conn.config_json)

        if not os.path.isfile(staging_path):
            raise RuntimeError("staging_file_missing")

        if extract_archive_flag:
            prefix = apply_connection_prefix(
                path_prefix,
                _extract_prefix_from_object_key(object_key or original_filename),
            )
            with tempfile.TemporaryDirectory(prefix="p2hs-ingest-") as tmp:
                extract_dir = os.path.join(tmp, "extracted")
                os.makedirs(extract_dir, exist_ok=True)
                extract_archive(staging_path, extract_dir)

                entries: list[tuple[str, str]] = []
                for dirpath, dirnames, filenames in os.walk(extract_dir):
                    dirnames[:] = [d for d in dirnames if d not in _SKIP_DIR_NAMES]
                    for fname in filenames:
                        if fname.startswith("._"):
                            continue
                        abs_path = os.path.join(dirpath, fname)
                        rel = os.path.relpath(abs_path, extract_dir).replace("\\", "/")
                        entries.append((abs_path, rel))

                total_entries = len(entries)
                used_flat_keys: dict[str, int] = {}
                async with db_factory() as db:
                    row = await StorageIngestJobRepository(db).get(ingest_id)
                    if row:
                        row.total_entries = total_entries
                        await StorageIngestJobRepository(db).save(row)
                        await db.commit()

                for idx, (abs_path, rel) in enumerate(entries, start=1):
                    base_key = object_key_for_extracted_file(
                        prefix=prefix,
                        relative=rel,
                        archive_path_mode=archive_path_mode,
                    )
                    key = base_key
                    if archive_path_mode == "flatten":
                        count = used_flat_keys.get(base_key, 0) + 1
                        used_flat_keys[base_key] = count
                        if count > 1:
                            stem, dot, ext = base_key.rpartition(".")
                            key = f"{stem}-{count}{dot}{ext}" if dot else f"{base_key}-{count}"
                    try:
                        upload_file_from_path(
                            account_id=account_id,
                            access_key_id=access_key,
                            secret_access_key=secret_key,
                            bucket=bucket,
                            key=key,
                            local_path=abs_path,
                        )
                        uploaded_keys.append(key)
                    except Exception as exc:
                        skipped.append(rel)
                        if not first_error:
                            first_error = str(exc)[:500]
                        if len(skipped) <= _MAX_SKIP_LOG:
                            logger.warning(
                                "storage_ingest_file_upload_failed",
                                job_id=job_id,
                                rel=rel,
                                error=str(exc)[:200],
                            )

                    if idx % 25 == 0 or idx == total_entries:
                        async with db_factory() as db:
                            row = await StorageIngestJobRepository(db).get(ingest_id)
                            if row:
                                row.uploaded_count = len(uploaded_keys)
                                row.skipped_count = len(skipped)
                                row.total_entries = total_entries
                                await StorageIngestJobRepository(db).save(row)
                                await db.commit()
        else:
            key = apply_connection_prefix(
                path_prefix,
                (object_key or original_filename).replace("\\", "/").lstrip("/"),
            )
            upload_file_from_path(
                account_id=account_id,
                access_key_id=access_key,
                secret_access_key=secret_key,
                bucket=bucket,
                key=key,
                local_path=staging_path,
            )
            uploaded_keys.append(key)
            total_entries = 1
            async with db_factory() as db:
                row = await StorageIngestJobRepository(db).get(ingest_id)
                if row:
                    row.total_entries = total_entries
                    await StorageIngestJobRepository(db).save(row)
                    await db.commit()

        all_failed = len(uploaded_keys) == 0 and len(skipped) > 0
        terminal_status = (
            StorageIngestJobStatus.FAILED.value
            if all_failed
            else StorageIngestJobStatus.COMPLETED.value
        )

        async with db_factory() as db:
            row = await StorageIngestJobRepository(db).get(ingest_id)
            if row:
                total = row.total_entries or len(uploaded_keys)
                row.status = terminal_status
                row.uploaded_count = len(uploaded_keys)
                row.skipped_count = len(skipped)
                row.total_entries = total
                row.result_json = {
                    "uploaded_count": len(uploaded_keys),
                    "skipped_count": len(skipped),
                    "first_write_error": first_error,
                    "sample_keys": uploaded_keys[:50],
                }
                row.error = first_error if all_failed else None
                await StorageIngestJobRepository(db).save(row)
                await db.commit()

        logger.info(
            "storage_ingest_completed",
            job_id=job_id,
            uploaded=len(uploaded_keys),
            skipped=len(skipped),
            status=terminal_status,
        )
        return {
            "status": terminal_status,
            "uploaded_count": len(uploaded_keys),
            "skipped_count": len(skipped),
        }

    except Exception as exc:
        logger.exception("storage_ingest_failed", job_id=job_id, error=str(exc))
        async with db_factory() as db:
            row = await StorageIngestJobRepository(db).get(ingest_id)
            if row:
                row.status = StorageIngestJobStatus.FAILED.value
                row.error = str(exc)[:500]
                row.uploaded_count = len(uploaded_keys)
                row.skipped_count = len(skipped)
                await StorageIngestJobRepository(db).save(row)
                await db.commit()
        return {"status": StorageIngestJobStatus.FAILED.value, "error": str(exc)[:500]}

    finally:
        try:
            if os.path.isfile(staging_path):
                os.remove(staging_path)
            parent = os.path.dirname(staging_path)
            if os.path.isdir(parent) and not os.listdir(parent):
                shutil.rmtree(parent, ignore_errors=True)
        except OSError:
            pass
