from __future__ import annotations

import hashlib
import io
import os
import time
import uuid
from typing import Optional, Dict, Any, Tuple

import pandas as pd
from fastapi import UploadFile, HTTPException

from ..api.intelligence_client import IntelligenceClient
from ..services.engine_client import EngineClient
from ..storage import LocalStorageBackend, StorageBackend

try:
    from ..storage.backends.s3_backend import (
        S3Backend as _S3Backend,
    )  # new pluggable backend
except Exception:
    _S3Backend = None  # optional
from ..repos.tradeshow_repo import TradeshowRepo
from ..services.metrics import metrics
from ..workers.tradeshow_worker import TradeshowWorker


def _env_int(name: str, default: int) -> int:
    try:
        return int(os.getenv(name, default))
    except Exception:
        return default


class TradeshowService:
    def __init__(self, db, storage: Optional[StorageBackend] = None):
        self.db = db
        self.repo = TradeshowRepo(db)
        self.storage = storage or self._make_storage()
        self.intel = IntelligenceClient()
        self.engine = EngineClient()

    def _make_storage(self) -> StorageBackend:
        provider = (
            os.getenv("STORAGE_PROVIDER") or os.getenv("STORAGE_BACKEND") or "local"
        ).lower()
        if provider in {"s3", "minio"} and _S3Backend is not None:
            return _S3Backend()
        # default local
        return LocalStorageBackend(base_path=os.path.join("temp", "tradeshow"))

    async def _size_guard(self, content_len: int) -> None:
        max_mb = _env_int("TRADESHOW_MAX_UPLOAD_MB", 50)
        if content_len > max_mb * 1024 * 1024:
            raise HTTPException(
                status_code=413, detail=f"File too large (>{max_mb} MB)"
            )

    @staticmethod
    def _normalize_utf8(data: bytes) -> bytes:
        try:
            text = data.decode("utf-8", errors="ignore")
        except Exception:
            text = data.decode("latin-1", errors="ignore")
        # Normalize newlines
        text = text.replace("\r\n", "\n").replace("\r", "\n")
        return text.encode("utf-8")

    @staticmethod
    def _content_hash(data: bytes, idem_key: Optional[str]) -> Tuple[str, str]:
        h = hashlib.sha256(data).hexdigest()
        idem = (idem_key or "").strip()
        key = f"{h}|{idem}"
        import_id = hashlib.sha1(key.encode("utf-8")).hexdigest()[:16]
        return h, import_id

    async def _save_bytes(
        self, key: str, content: bytes, content_type: str = "text/csv"
    ) -> str:
        # Wrap in UploadFile compatible object
        up = UploadFile(
            filename=os.path.basename(key),
            file=io.BytesIO(content),
            content_type=content_type,
        )
        return await self.storage.save(up, key)

    async def accept_import(
        self, file: UploadFile, idem_key: Optional[str], account_id: str
    ) -> Dict[str, Any]:
        raw = await file.read()
        await self._size_guard(len(raw))
        data: bytes = self._normalize_utf8(raw)

        # Basic header validation for CSV only (XLSX handled later by pandas)
        filename_lower = (file.filename or "").lower()
        headers: list[str] = []
        if not (filename_lower.endswith(".xlsx") or filename_lower.endswith(".xls")):
            first_line = (
                data.split(b"\n", 1)[0].decode("utf-8", errors="ignore").strip()
            )
            headers = [h for h in first_line.split(",") if h.strip()]
            if len(headers) == 0:
                raise HTTPException(status_code=400, detail="CSV has no headers")

        c_hash, import_id = self._content_hash(data, idem_key)

        # Idempotency: short-circuit if existing for this tenant and idem_key
        existing = await self.repo.get_import_by_key(
            account_id=account_id,
            content_hash=c_hash,
            idem_key=idempotency_key_to_none(idem_key),
        )
        if existing:
            artifact_url = await self.storage.generate_presigned_url(
                existing.get("artifact_key"), expires_in=24 * 3600
            )
            return {
                "import_id": existing["id"],
                "sample_rows": int(
                    (existing.get("meta") or {}).get("rows_sampled") or 0
                ),
                "config_version": str(existing.get("config_version") or "v0"),
                "artifact_url": artifact_url,
                "message": "import accepted",
            }

        # Build keys and persist artifacts
        original_key = f"{import_id}/original.csv"
        sample_key = f"{import_id}/sample.csv"
        await self._save_bytes(original_key, data, file.content_type or "text/csv")

        # Build a small sample (<= 5000 rows) using normalized UTF-8
        head_lines = data.decode("utf-8", errors="ignore").splitlines()[:5001]
        sample_bytes = "\n".join(head_lines).encode("utf-8", errors="ignore")
        await self._save_bytes(sample_key, sample_bytes, "text/csv")

        # Prepare minimal pandas DataFrames schema for auto-configure
        try:
            # Parse headers for schema, support CSV and XLSX minimally
            filename = filename_lower
            if filename.endswith(".xlsx") or filename.endswith(".xls"):
                df = pd.read_excel(io.BytesIO(sample_bytes), nrows=0)
            else:
                df = pd.read_csv(io.BytesIO(sample_bytes), nrows=0)
        except Exception:
            # Fallback: CSV-style header from first line of sample
            try:
                headers = head_lines[0].split(",") if head_lines else []
            except Exception:
                headers = []
            df = pd.DataFrame(columns=headers)

        cfg = await self.intel.auto_configure(
            df, df, goal=os.getenv("TRADESHOW_GOAL", "balanced")
        )
        config_version = str(cfg.get("version") or cfg.get("config_version") or "v0")

        # Persist import record
        meta = {
            "columns": df.columns.tolist(),
            "rows_sampled": max(0, len(head_lines) - 1),
        }
        await self.repo.upsert_import_record(
            import_id=import_id,
            account_id=account_id,
            content_hash=c_hash,
            idem_key=idempotency_key_to_none(idem_key),
            meta=meta,
            status="accepted",
            config_version=config_version,
            artifact_key=original_key,
            sample_key=sample_key,
        )

        try:
            # Telemetry: job_received on import accept
            await metrics.increment("job_received")
        except Exception:
            pass

        # Signed URL for original artifact (24h)
        artifact_url = await self.storage.generate_presigned_url(
            original_key, expires_in=24 * 3600
        )

        return {
            "import_id": import_id,
            "sample_rows": meta["rows_sampled"],
            "config_version": config_version,
            "artifact_url": artifact_url,
            "message": "import accepted",
        }

    async def commit(self, import_id: str, account_id: str) -> Dict[str, Any]:
        row = await self.repo.get_import(import_id)
        if not row:
            raise HTTPException(status_code=404, detail="import not found")
        if str(row.get("account_id")) != str(account_id):
            raise HTTPException(status_code=404, detail="import not found")

        # Kick off a job with the engine (golden path). For MVP, return a stub job id.
        job_id = f"ts_{import_id}_{int(time.time())}"

        # Optionally, record a starter report entry that will be updated later
        await self.repo.save_report(
            job_id=job_id,
            account_id=account_id,
            matched_count=0,
            new_count=0,
            undo_token=str(uuid.uuid4()),
            summary={"status": "running"},
        )
        try:
            await metrics.increment("job_started")
        except Exception:
            pass

        # Launch background worker (fire-and-forget)
        try:
            import asyncio

            worker = TradeshowWorker(self.db, storage=self.storage)
            asyncio.create_task(worker.run_job(job_id))
        except Exception:
            # If scheduling fails, leave report as running
            pass
        return {"job_id": job_id, "status": "accepted"}

    async def report(self, job_id: str) -> Dict[str, Any]:
        rep = await self.repo.get_report(job_id)
        if not rep:
            raise HTTPException(status_code=404, detail="report not ready")
        return rep


def idempotency_key_to_none(k: Optional[str]) -> Optional[str]:
    v = (k or "").strip()
    return v or None
