#!/usr/bin/env node
// Jellyfin CLI — token-efficient output for assistant use
const https = require('https');
const fs = require('fs');
const path = require('path');

// Load credentials from unified services.env
const credFile = process.env.CRED_FILE || path.join(__dirname, '..', '.credentials', 'services.env');
const envFile = fs.readFileSync(credFile, 'utf8');
const env = {};
envFile.split('\n').forEach(l => { const m = l.match(/^(\w+)=(.+)$/); if (m) env[m[1]] = m[2]; });

const BASE = env.JELLYFIN_URL;
const TOKEN_CACHE = path.join(__dirname, '..', '.credentials', '.jellyfin-token.json');

function request(method, endpoint, body) {
  return new Promise((resolve, reject) => {
    const sep = endpoint.includes('?') ? '&' : '?';
    const u = new URL(`${BASE}${endpoint}`);
    const opts = {
      hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search,
      method, headers: {}
    };
    if (body) {
      const data = JSON.stringify(body);
      opts.headers['Content-Type'] = 'application/json';
      opts.headers['Content-Length'] = Buffer.byteLength(data);
    }
    const req = https.request(opts, res => {
      let d = '';
      res.on('data', c => d += c);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, data: JSON.parse(d) }); }
        catch { resolve({ status: res.statusCode, data: d }); }
      });
    });
    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

async function authenticate() {
  // Check cache first
  try {
    const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE, 'utf8'));
    if (cached.token && cached.userId) {
      // Verify token still works
      const test = await apiWith(cached.token, cached.userId, `/Users/${cached.userId}`);
      if (test) return cached;
    }
  } catch {}

  // Authenticate
  const res = await request('POST', '/Users/AuthenticateByName', {
    Username: env.JELLYFIN_USER,
    Pw: env.JELLYFIN_PASS
  });

  // Add auth header for the request
  const authRes = await new Promise((resolve, reject) => {
    const body = JSON.stringify({ Username: env.JELLYFIN_USER, Pw: env.JELLYFIN_PASS });
    const u = new URL(`${BASE}/Users/AuthenticateByName`);
    const req = https.request({
      hostname: u.hostname, port: u.port || 443, path: u.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        'X-Emby-Authorization': 'MediaBrowser Client="Hoid", Device="CLI", DeviceId="hoid-cli-1", Version="1.0"'
      }
    }, res => {
      let d = '';
      res.on('data', c => d += c);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, data: JSON.parse(d) }); }
        catch { resolve({ status: res.statusCode, data: d }); }
      });
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });

  if (authRes.status !== 200 || !authRes.data.AccessToken) {
    console.error(`Auth failed (${authRes.status}):`, typeof authRes.data === 'string' ? authRes.data : JSON.stringify(authRes.data).slice(0, 200));
    process.exit(1);
  }

  const result = { token: authRes.data.AccessToken, userId: authRes.data.User.Id };
  fs.writeFileSync(TOKEN_CACHE, JSON.stringify(result));
  return result;
}

function apiWith(token, userId, endpoint) {
  const sep = endpoint.includes('?') ? '&' : '?';
  const url = `${BASE}${endpoint}`;
  return new Promise((resolve, reject) => {
    const u = new URL(url);
    https.get({
      hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search,
      headers: { 'X-Emby-Token': token }
    }, res => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        if (res.statusCode === 401) return resolve(null);
        try { resolve(JSON.parse(data)); } catch { resolve(data); }
      });
    }).on('error', reject);
  });
}

let _auth;
async function api(endpoint) {
  if (!_auth) _auth = await authenticate();
  const result = await apiWith(_auth.token, _auth.userId, endpoint);
  if (result === null) {
    // Token expired, re-auth
    try { fs.unlinkSync(TOKEN_CACHE); } catch {}
    _auth = await authenticate();
    return apiWith(_auth.token, _auth.userId, endpoint);
  }
  return result;
}

async function getUserId() {
  if (!_auth) _auth = await authenticate();
  return _auth.userId;
}

function dur(ticks) {
  if (!ticks) return '';
  const min = Math.round(ticks / 600000000);
  if (min < 60) return `${min}m`;
  return `${Math.floor(min/60)}h${min%60 ? ' '+min%60+'m' : ''}`;
}

function pct(userData, runTimeTicks) {
  if (!userData) return '';
  if (userData.Played) return '✓';
  if (userData.PlayedPercentage) return Math.round(userData.PlayedPercentage) + '%';
  if (userData.PlaybackPositionTicks && runTimeTicks) return Math.round(userData.PlaybackPositionTicks / runTimeTicks * 100) + '%';
  return '';
}

const commands = {
  async resume(limit = 10) {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items/Resume?Limit=${limit}&Fields=Overview,RunTimeTicks&MediaTypes=Video`);
    if (!r.Items || !r.Items.length) return console.log('Nothing in progress');
    console.log('TITLE\tEPISODE\tPROGRESS\tDURATION');
    r.Items.forEach(i => {
      const show = i.SeriesName || '';
      console.log([show || i.Name, show ? i.Name : '', pct(i.UserData, i.RunTimeTicks), dur(i.RunTimeTicks)].join('\t'));
    });
  },

  async recent(limit = 10) {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items/Latest?Limit=${limit}&Fields=RunTimeTicks`);
    if (!r || !r.length) return console.log('No recent items');
    console.log('TYPE\tTITLE\tNAME\tYEAR\tDURATION');
    r.forEach(i => {
      console.log([i.Type === 'Episode' ? 'EP' : 'MOV', i.SeriesName || i.Name, i.SeriesName ? i.Name : '', i.ProductionYear || '', dur(i.RunTimeTicks)].join('\t'));
    });
  },

  async watched(limit = 20) {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items?Limit=${limit}&SortBy=DatePlayed&SortOrder=Descending&Filters=IsPlayed&Recursive=true&IncludeItemTypes=Movie,Episode&Fields=RunTimeTicks,DateLastMediaAdded`);
    if (!r.Items || !r.Items.length) return console.log('No watched items');
    console.log('TYPE\tTITLE\tEPISODE\tYEAR');
    r.Items.forEach(i => {
      console.log([i.Type === 'Episode' ? 'EP' : 'MOV', i.SeriesName || i.Name, i.SeriesName ? i.Name : '', i.ProductionYear || ''].join('\t'));
    });
  },

  async shows() {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true&SortBy=SortName&Fields=RunTimeTicks`);
    if (!r.Items || !r.Items.length) return console.log('No shows');
    console.log('TITLE\tYEAR\tSTATUS');
    r.Items.forEach(i => {
      console.log([i.Name, i.ProductionYear || '', pct(i.UserData, i.RunTimeTicks) || 'unwatched'].join('\t'));
    });
  },

  async movies() {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&SortBy=SortName&Fields=RunTimeTicks`);
    if (!r.Items || !r.Items.length) return console.log('No movies');
    console.log('TITLE\tYEAR\tDURATION\tSTATUS');
    r.Items.forEach(i => {
      console.log([i.Name, i.ProductionYear || '', dur(i.RunTimeTicks), pct(i.UserData, i.RunTimeTicks) || '-'].join('\t'));
    });
  },

  async search(query) {
    if (!query) { console.error('Usage: jellyfin search <query>'); process.exit(1); }
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Items?SearchTerm=${encodeURIComponent(query)}&Recursive=true&IncludeItemTypes=Movie,Series,Episode&Limit=10&Fields=RunTimeTicks`);
    if (!r.Items || !r.Items.length) return console.log('No results');
    console.log('TYPE\tTITLE\tNAME\tYEAR');
    r.Items.forEach(i => {
      console.log([i.Type, i.SeriesName || i.Name, i.SeriesName ? i.Name : '', i.ProductionYear || ''].join('\t'));
    });
  },

  async stats() {
    const uid = await getUserId();
    const [movies, shows, episodes] = await Promise.all([
      api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&Filters=IsPlayed`),
      api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true`),
      api(`/Users/${uid}/Items?IncludeItemTypes=Episode&Recursive=true&Filters=IsPlayed`),
    ]);
    console.log(`Movies watched:\t${movies.TotalRecordCount}`);
    console.log(`Shows in library:\t${shows.TotalRecordCount}`);
    console.log(`Episodes watched:\t${episodes.TotalRecordCount}`);
  },

  async library(limitArg) {
    const uid = await getUserId();
    const full = process.argv.includes('--full');
    const [movies, shows] = await Promise.all([
      api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&SortBy=SortName&Fields=Genres,OfficialRating,Overview,RunTimeTicks&Limit=${limitArg && limitArg !== '--full' ? limitArg : 10000}`),
      api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true&SortBy=SortName&Fields=Genres,OfficialRating,Overview,RunTimeTicks&Limit=${limitArg && limitArg !== '--full' ? limitArg : 10000}`)
    ]);
    console.log('TYPE\tTITLE\tYEAR\tGENRES\tRATING\tDURATION\tSTATUS\tDESCRIPTION');
    const printItem = (type, i) => {
      const genres = (i.Genres || []).join(',') || '-';
      const rating = i.OfficialRating || '-';
      const overview = i.Overview || '';
      const desc = full ? overview.replace(/[\t\n\r]/g, ' ') : (overview.length > 100 ? overview.slice(0, 100).replace(/[\t\n\r]/g, ' ') + '...' : overview.replace(/[\t\n\r]/g, ' ')) || '-';
      const status = pct(i.UserData, i.RunTimeTicks) || '-';
      console.log([type, i.Name, i.ProductionYear || '', genres, rating, dur(i.RunTimeTicks), status, desc].join('\t'));
    };
    (movies.Items || []).forEach(i => printItem('MOV', i));
    (shows.Items || []).forEach(i => printItem('SHOW', i));
  },

  async genres() {
    const uid = await getUserId();
    const [movies, shows] = await Promise.all([
      api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&Fields=Genres&Limit=10000`),
      api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true&Fields=Genres&Limit=10000`)
    ]);
    const counts = {};
    [...(movies.Items || []), ...(shows.Items || [])].forEach(i => {
      (i.Genres || []).forEach(g => { counts[g] = (counts[g] || 0) + 1; });
    });
    Object.entries(counts).sort((a, b) => b[1] - a[1]).forEach(([g, c]) => console.log(`${g}\t${c}`));
  },

  async libraries() {
    const uid = await getUserId();
    const r = await api(`/Users/${uid}/Views`);
    console.log('NAME\tTYPE\tID');
    (r.Items || []).forEach(l => {
      console.log([l.Name, l.CollectionType || 'mixed', l.Id].join('\t'));
    });
  }
};

const cmd = process.argv[2];
const args = process.argv.slice(3);

if (!cmd || !commands[cmd]) {
  console.log('Usage: jellyfin <command> [args]\n');
  console.log('Commands:');
  console.log('  resume [limit]     Continue watching');
  console.log('  recent [limit]     Recently added');
  console.log('  watched [limit]    Watch history');
  console.log('  shows              All series');
  console.log('  movies             All movies');
  console.log('  search <query>     Search library');
  console.log('  stats              Watch statistics');
  console.log('  library [limit]    All movies & shows with metadata (--full for full descriptions)');
  console.log('  genres             List genres with counts');
  console.log('  libraries          List libraries');
  process.exit(0);
}

commands[cmd](...args).catch(e => { console.error(e.message); process.exit(1); });
